67 Commits

Author SHA1 Message Date
3dtours f16467eba1 fix: đã sửa lỗi back và rewind 2026-07-26 12:27:08 +07:00
3dtours b1bbf567ab fix(piano-roll): seek-to-start restarts playback
- Add seekPlaybackTo(time) that stops, updates refs, reschedules
  MIDI, and restarts playback for both sub-tab and main timeline
- 'Quay lại đầu' and 'Đầu vùng chọn' buttons use seekPlaybackTo
2026-07-26 12:22:51 +07:00
3dtours 8204bc016d fix: đã sửa lỗi stop và nút loop 2026-07-26 12:21:02 +07:00
3dtours b0d7f35a1e fix(piano-roll): loop only on btn + instant stop
- Sub-tab selection loop only active when st.isLooping is true
- End-of-playback loop uses st.isLooping, not global isLoopingSelection
- SonicSF.stopAll() sets gain 0 and stops oscillators immediately
2026-07-26 12:16:01 +07:00
3dtours 653c0747cb fix: đã sửa lỗi click vị trí mới cho playhead 2026-07-26 12:12:48 +07:00
3dtours 4e880850f5 fix(piano-roll): playhead seek + sound cracking
- Piano roll ruler click during playback now seeks and continues playing
- ADSR envelope uses linearRampToValueAtTime for crack-free release
- stopAll ramps gain to 0 in 20ms before stopping oscillators
2026-07-26 12:03:29 +07:00
3dtours 1ffdec68c6 fix: ruler click 200ms timeout + default instrument attackTime 0.03 + safe ramp 2026-07-26 11:51:50 +07:00
3dtours 523a754949 fix: remove grab handle e.stopPropagation blocking ruler clicks 2026-07-26 11:48:21 +07:00
3dtours 79999065a7 fix: ruler click moves playhead, drag moves range 2026-07-26 11:45:47 +07:00
3dtours 49c9f019db wiki: ruler range drag + SonicSF smooth stopAll 2026-07-26 11:42:17 +07:00
3dtours e18b261943 fix: ruler range drag/move, default cursor, SonicSF smooth stopAll 2026-07-26 11:42:06 +07:00
3dtours d3c28d2fa7 wiki: fix ruler click playhead 2026-07-26 11:37:00 +07:00
3dtours 7601cc72c9 fix: ruler click playhead update using available props onStop+onPlayPause 2026-07-26 11:36:55 +07:00
3dtours f7b499cda9 wiki: fix range auto-loop + gain fade-out 2026-07-26 11:34:08 +07:00
3dtours 5e096b5594 fix: remove range auto-loop + gain fade-out for reschedule crackling 2026-07-26 11:34:02 +07:00
3dtours 724b42d506 wiki: fix realtime velocity + ruler restart 2026-07-26 11:28:54 +07:00
3dtours d3a3adca8a fix: realtime velocity with updatedNotes + ruler restart without onStop 2026-07-26 11:28:47 +07:00
3dtours 7ede9bbf72 wiki: realtime velocity, ruler play, export MIDI 2026-07-26 11:20:25 +07:00
3dtours b6b1faf083 feat: realtime velocity, ruler play click, export MIDI button 2026-07-26 11:20:19 +07:00
3dtours bf4f2a5f11 fix: đã sửa lỗi thay đổi instrument thì bị stop 2026-07-26 11:15:09 +07:00
3dtours 90ca8f484b fix: restore isPlaying after stopAllPlayback on instrument change 2026-07-26 11:13:50 +07:00
3dtours 6926c79357 wiki: instrument change loop fix 2026-07-26 11:10:41 +07:00
3dtours 9717470bb6 fix: instrument change keeps loop playback alive 2026-07-26 11:10:31 +07:00
3dtours e57f6d96d9 fix: MIDI tab real-time instrument change - restart playback 2026-07-26 11:07:37 +07:00
3dtours 5b500ebdf3 fix: remove MIDI tab loop button + Ctrl+click clear range 2026-07-26 11:03:51 +07:00
3dtours 9438f44052 fix: CC note detection with Y-aware stem matching 2026-07-26 10:59:33 +07:00
3dtours 3901af75b2 fix: đã sửa lỗi vẽ tiếp khi thay đổi pitch 2026-07-26 10:57:01 +07:00
3dtours 27d9556a9d fix: brush draw pitch revisit by comparing lastDrawnPitch 2026-07-26 10:55:21 +07:00
3dtours bc0c2d3279 fix: đã sửa lỗi vẽ lên và trường độ 2026-07-26 10:53:08 +07:00
3dtours 034455598d fix: brush draw per-note duration with noteStartBeats tracking 2026-07-26 10:50:45 +07:00
3dtours 8450bd6de4 fix: brush draw only pitch change + CC velocity skip unchanged 2026-07-26 10:48:16 +07:00
3dtours d2993eee69 fix: brush drawing smooth - track pitch + beat delta 2026-07-26 10:44:37 +07:00
3dtours fe379c45e2 fix: brush shape draw + remove transport + loop repeat sound 2026-07-26 10:41:31 +07:00
3dtours 3ae48f2f2f fix: đã sửa lỗi bị mất không hiển thị midi và vẽ cclane 2026-07-26 10:37:41 +07:00
3dtours d20fafb75e fix: velocity flicker - grid canvas useLayoutEffect sync 2026-07-26 10:29:23 +07:00
3dtours 0b7dd82407 fix: scroll speed matches mouse + CC flicker useLayoutEffect 2026-07-26 10:26:00 +07:00
3dtours 7107a0cc14 fix: brush scroll speed + CC snap + non-selected flicker 2026-07-26 10:21:31 +07:00
3dtours 8ec4b05e21 fix: brush pitch-based scroll + CC velocity per-note paint 2026-07-26 10:14:40 +07:00
3dtours 9237d01f35 fix: auto-scroll continuous + batch CC velocity for selected notes 2026-07-26 10:09:55 +07:00
3dtours a7417b9bfa fix: đã sửa lỗi bị mất không hiển thị midi piano grid 2026-07-26 10:05:23 +07:00
3dtours a765998455 feat: piano roll 4 features - SNAP, velocity selected, auto-scroll, synth button 2026-07-26 09:43:19 +07:00
3dtours bd55d035de fix: đã sửa lỗi MIDI loop 2026-07-26 07:16:17 +07:00
3dtours 8516961d80 fix: đã sửa lỗi chọn range tự do 2026-07-25 22:52:54 +07:00
3dtours c917db200e fix: ruler doc-level drag, remove onMouseMove/onMouseLeave from element, comma fix 2026-07-25 22:50:16 +07:00
3dtours e5196e79c8 fix: use refs for selectionMarquee and notes in doc-level drag handler to avoid stale closure 2026-07-25 22:37:30 +07:00
3dtours ce30289f46 fix: loop reschedule midi, note select in doc-level handler, ctrl+click ruler clear loop 2026-07-25 22:35:23 +07:00
3dtours 5bd92ee186 fix: đã sửa lỗi chọn range trong MIDI marquee và chọn note 2026-07-25 22:34:34 +07:00
3dtours 89f2fed1d9 fix: blue marquee for note select on grid, green loop overlay from ruler drag, notes play once 2026-07-25 22:31:42 +07:00
3dtours 4306ff302a fix: marquee survive mouseleave, loop reschedule midi notes, separate leave/up handlers 2026-07-25 22:20:58 +07:00
3dtours 790a5823f6 fix: đã sửa lỗi chọn range trong MIDI marquee 2026-07-25 22:18:44 +07:00
3dtours fccdb15ad7 fix: piano roll selection marquee green full-height, edge resize, body move, playback min 16 beats, doc-level drag 2026-07-25 22:13:50 +07:00
3dtours a181f7a24d fix: đã sửa lỗi chọn range trong MIDI tab 2026-07-25 21:30:17 +07:00
3dtours 1a5c1eff43 fix: doc-level ruler drag, min 16bars playback, selection overlay, edge/center handles 2026-07-25 21:28:26 +07:00
3dtours 9bfcd1870c fix: loop range playback via st.selectionStart/End + isLooping 2026-07-25 21:21:03 +07:00
3dtours 39680cb95c fix: ruler close needed 4 parens (was 3), closing parent div properly 2026-07-25 21:14:44 +07:00
3dtours 2ce731e6a9 fix: Ctrl+S piano roll + ruler drag selection + paren balance 2026-07-25 21:09:34 +07:00
3dtours 025aba3265 fix: Ctrl+S in piano roll + ruler range selection via drag 2026-07-25 21:04:19 +07:00
3dtours 57fd110fe3 fix: add capture flag to Ctrl+S keydown handler to prevent browser save 2026-07-25 20:57:43 +07:00
3dtours c1a3d3a6dd feat: Ctrl+S saves current tab (piano roll, section, audio) or project + all dirty tabs 2026-07-25 20:54:23 +07:00
3dtours fde9a91c9b fix: đã sửa lỗi MIDI note bị flicker 2026-07-25 20:46:53 +07:00
3dtours f2d02a358f fix: skip st.notes sync when draggedNote active (prevents brush flicker) 2026-07-25 20:45:24 +07:00
3dtours ce319573ab fix: move Ctrl+A useEffect after selectedNoteIds declaration 2026-07-25 20:39:54 +07:00
3dtours 29b682e3ba fix: move Ctrl+A useEffect after notes declaration to avoid TDZ error 2026-07-25 20:38:53 +07:00
3dtours f4ec769650 fix: restore useEffect sync for st.notes, remove render-phase TDZ error 2026-07-25 20:36:21 +07:00
3dtours 966e673fae fix: replace sync useEffect with render-phase guard to prevent flicker 2026-07-25 20:31:17 +07:00
3dtours a9f56d0fcd fix: drag clamp, pitch anchor, Ctrl+A, marquee dir, resize snap 2026-07-25 20:30:01 +07:00
3dtours 8d83a46e08 fix: MIDI item playback respects visual duration + scale flicker guard 2026-07-25 20:14:53 +07:00
10 changed files with 1929 additions and 358 deletions
@@ -0,0 +1,212 @@
# Piano Roll: 4 tính năng
## 1. Auto-scroll brush khi drag gần cạnh
**File**: `app/static/js/app.jsx`
**Vị trí**: Trong `handleGridMouseMove`, cuối block `draggedNote.mode === 'draw'` (trước `return;` ở dòng ~5237).
**Code thêm** (sau visitedPitches/brushIds logic, trước `return;`):
```js
const container = gridScrollRef.current;
if (container) {
const cr = container.getBoundingClientRect();
const edgeThreshold = 30;
const scrollStep = 6;
if (e.clientY < cr.top + edgeThreshold) {
container.scrollTop = Math.max(0, container.scrollTop - scrollStep);
} else if (e.clientY > cr.bottom - edgeThreshold) {
container.scrollTop = Math.min(container.scrollHeight - container.clientHeight, container.scrollTop + scrollStep);
}
}
```
**⚠ Edge case**: Nếu chuột dừng tại mép, `mousemove` ngưng → cuộn dừng. Để cuộn liên tục, dùng `setInterval` khi vào threshold. Tạm thời chấp nhập giới hạn này.
---
## 2. Ctrl+drag velocity với selected notes
**File**: `app/static/js/app.jsx`
### 2a. `handleCCMouseDown` (dòng ~5360)
Thay block `if (e.ctrlKey)` hiện tại:
```js
if (e.ctrlKey) {
if (selectedNoteIds.length > 0) {
selectedNoteIds.forEach(id => {
const idx = notes.findIndex(n => n.id === id);
if (idx !== -1) paintNote(idx, val);
});
ccDragRef.current = { active: true, lastBeat: beat, selectedMode: true, lastPainted: selectedNoteIds.map(id => notes.findIndex(n => n.id === id)).filter(i => i !== -1) };
} else {
if (noteIdx !== -1) paintNote(noteIdx, val);
ccDragRef.current = { active: true, lastBeat: beat, lastPainted: noteIdx !== -1 ? [noteIdx] : [] };
}
return;
}
```
### 2b. `handleCCMouseMove` (sau dòng ~5381)
Thêm block đầu `handleCCMouseMove` (SAU khi lấy `drag`, `painted`, TRƯỚC `candidateIdx`):
```js
if (drag.selectedMode && selectedNoteIds.length > 0) {
selectedNoteIds.forEach(id => {
const idx = notes.findIndex(n => n.id === id);
if (idx !== -1 && !painted.includes(idx)) {
setNotes(prev => prev.map((n, i) => {
if (i !== idx) return n;
if (ccMode === 'pan') return { ...n, pan: (val - 0.5) * 2.0 };
return { ...n, velocity: val };
}));
}
});
// Cập nhật lastPainted TRỰC TIẾP trên ref (không qua setNotes callback)
const newPainted = selectedNoteIds
.map(id => notes.findIndex(n => n.id === id))
.filter(i => i !== -1 && !painted.includes(i));
ccDragRef.current.lastPainted = [...painted, ...newPainted];
return;
}
```
**Không gán `drag.lastPainted` bên trong `setNotes` callback**`drag``ccDragRef.current`, gán trực tiếp vào ref ngoài callback để tránh stale closure.
### 2b2. Cleanup `selectedMode` khi mouseup (dòng ~5409)
Trong `handleCCMouseUp` (và `onMouseLeave`), thêm reset:
```js
if (ccDragRef.current) ccDragRef.current.selectedMode = false;
```
### 2c. CC canvas rendering (dòng ~4911-4932)
Thêm `isSelected` vào loop notes; đổi màu xanh dương `#3b82f6` khi selected:
```js
const isSelected = selectedNoteIds.includes(note.id);
// ...
ctx.strokeStyle = ccMode === 'pan' ? (isSelected ? '#60a5fa' : '#a78bfa') : (isSelected ? '#3b82f6' : '#fbbf24');
ctx.fillStyle = ccMode === 'pan' ? (isSelected ? '#3b82f6' : '#c084fc') : (isSelected ? '#3b82f6' : '#fbbf24');
```
Thêm `selectedNoteIds` vào dependency array của effect.
---
## 3. SNAP trong MIDI tab
**Ghi chú**: `getSnapBeat(beat, mode)` đã xử lý `mode === 'free'` bằng cách return `beat` không đổi. Không cần check `snapVal !== 'free'` riêng.
**File**: `app/static/js/app.jsx`
### 3a. Selection marquee — create (dòng ~5003-5008)
Snap `startBeat` khi tạo marquee:
```js
const snapStart = getSnapBeat(beat, snapVal);
setSelectionMarquee({
startBeat: snapStart, startPitch: pitch,
currentBeat: snapStart, currentPitch: pitch
});
```
### 3a2. Selection marquee — drag update (dòng ~5140-5144)
Snap `currentBeat` khi kéo marquee:
```js
const snappedBeat = getSnapBeat(beat, snapVal);
const marquee = {
...selectionMarquee,
currentBeat: snappedBeat,
currentPitch: pitch
};
```
### 3b. Ruler drag loop range (dòng ~5742-5763)
Snap `clickBeat` khởi tạo, snap `beat` trong onMove:
```js
const snappedStartBeat = getSnapBeat(clickBeat, snapVal);
const startData = { startX: e.clientX, startBeat: snappedStartBeat, scrollLeft: e.currentTarget.scrollLeft };
// ...
const rawBeat = Math.max(0, bx / pixelsPerBeat);
const beat = getSnapBeat(rawBeat, snapVal);
```
### 3c. Shift+Click ruler loop (dòng ~5732-5739)
Đổi `Math.round(clickBeat / 4) * 4` thành `getSnapBeat(clickBeat, snapVal)`:
```js
const beatSnap = getSnapBeat(clickBeat, snapVal);
```
### 3d. Ruler loop handles (dòng ~5791, ~5808, ~5829)
Đổi `Math.round(bx / pixelsPerBeat / 4) * 4` thành `getSnapBeat(bx / pixelsPerBeat, snapVal)` ở cả 3 handle (left resize, right resize, grab body).
---
## 4. Synth button trong MIDI tab toolbar
**File**: `app/static/js/app.jsx`
### 4a. Prop `onInstrumentSelect` (dòng ~4546)
Thêm `onInstrumentSelect` vào props destructuring.
### 4b. Button synth trong toolbar (dòng ~5662-5700)
Chèn button sau MIDI input select, trước transport buttons:
```jsx
React.createElement("button", {
onClick: () => onInstrumentSelect && onInstrumentSelect(st.trackId),
title: st.instrumentName || "Synth",
className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 max-w-[50px] ${st.instrumentName ? 'bg-violet-900 text-violet-300 border-violet-700' : 'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`
}, React.createElement("i", { "data-lucide": "music", className: "w-3 h-3 shrink-0" }), React.createElement("span", { className: "truncate text-[9px]" }, st.instrumentName || 'Synth'))
```
### 4c. Pass callback từ App (dòng ~15631-15652)
Thêm `onInstrumentSelect: (trackId) => { openInstrumentSelector(trackId); }`.
### 4d. Đồng bộ subTab instrument (dòng ~6343-6353)
Trong `setTrackInstrumentWithProgram`, thêm cập nhật `subTabs`:
```js
setSubTabs(prev => prev.map(s => {
if (s.trackId !== trackId) return s;
return { ...s, instrumentProgram: programNumber !== undefined ? programNumber : undefined, instrumentName: displayName, instrumentId };
}));
```
---
## 5. Layout fix: thêm `h-full` (QUAN TRỌNG)
**File**: `app/static/js/app.jsx`, dòng ~5623
Đổi `className` của outer div từ:
```
"flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col"
```
thành:
```
"flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col h-full"
```
**Lý do**: `h-full` cung cấp height tham chiếu cho flex chain, tránh content area cao 0px.
---
## Thứ tự thực hiện
1. Sửa layout: thêm `h-full`
2. Feature 3: SNAP (4 edits nhỏ — dễ verify)
3. Feature 2: Velocity selected notes
4. Feature 1: Auto-scroll brush
5. Feature 4: Synth button (liên quan nhiều component nhất)
## Kiểm tra
```bash
cd /home/locpham/SonicForgeStudio && npm run build
```
Build phải pass. Nếu lỗi paren, kiểm tra đóng `()` tại cuối return statement.
@@ -0,0 +1,193 @@
# Plan: Apply & Install `md/34_VST_LINUX.md` (Revised per `md/35_VST_PLAN.md` + `md/35.1_VST_FIX.md`)
## Context Summary
- **Dockerfile** already has `libgl1`, `libasound2`, `libjack-jackd2-0`, `libfreetype6`, Xvfb, fluidsynth. Missing `libcurl4`.
- `requirements.txt` has `mido` but missing `sf2utils`.
- `app/core/vst_engine.py` has `PluginManager` with FluidSynth C-API-based `list_soundfont_instruments()`.
- `app/core/render_engine.py` FluidSynth path hardcodes `program_select(0, fid, 0, 0)`.
- `app/static/js/services/aiGateway.js``generate_multitrack_midi` tool has NO `soundfont_id`/`soundfont_bank`/`soundfont_program`.
- `soundfontPlayer.js` — oscillator-based, uses `program` only for ADSR/osc-type selection; no `controllerChange()` or `programChange()` channel-state methods.
- No `soundfont_catalog.json` generation or catalog API endpoint.
- `docker-compose.yml` mounts `.` to `/app`.
---
## 5 Mandatory Refinements (from 35_VST_PLAN.md)
1. **Condensed Catalog for AI**: `get_condensed_catalog_summary()` → max 4050 instruments categorized by GM groups (Piano, Organ, Guitar, Bass, Strings, Ensemble, Brass, Reed, Pipe, Synth Lead, Synth Pad, Drum Kit). Avoids token overflow.
2. **Non-Blocking Catalog Generation**: `generate_full_catalog()` runs on first API access, not on startup. Cache in memory; refresh on SF2 upload via background task.
3. **Dual Directory Scanning**: system `/opt/daw_engine/soundfonts/` AND user uploads `app/storage/uploads/soundfonts/`.
4. **DecentSampler CWD Fix**: `os.chdir()` to `.dspreset` parent directory before `load_preset()`, so relative `samples/*.wav` paths resolve.
5. **Robust Error Handling**: per-file `try/except` in `SoundFontInspector` — skip corrupted SF2 files with warning instead of crashing.
---
## Task A: SoundFont Inspection Engine (sf2utils)
### A1 — Add dependency
- Add `sf2utils>=0.9.0` to `requirements.txt`.
### A2 — Create `app/core/soundfont_inspector.py`
- `inspect_sf2_file(filepath)` — wrapped in `try/except` per Refinement 5. Returns `{soundfont_id, filename, total_instruments, instruments: [{bank, program, name, is_percussion}]}`.
- `generate_full_catalog(output_json_path)` — scans system dir `/opt/daw_engine/soundfonts/` + user upload dir (Refinement 3). Writes `soundfont_catalog.json`.
- `get_condensed_catalog_summary()` — returns categorized dict with ≤50 entries per Refinement 1. Groups instruments by GM category (Piano=0-7, Chromatic Perc=8-15, Organ=16-23, Guitar=24-31, Bass=32-39, Strings=40-47, Ensemble=48-55, Brass=56-63, Reed=64-71, Pipe=72-79, Synth Lead=80-89, Synth Pad=90-103, Drum Kit=128).
- `invalidate_catalog_cache()` — resets in-memory cache; called after SF2 upload.
### A3 — API Endpoint `GET /api/v1/plugins/soundfonts/catalog`
- In `app/api/v1/plugins.py`:
- Response shape: `{ full_catalog: {...}, condensed_catalog: {...} }`.
- Lazily generate on first call, cache in memory (Refinement 2).
- `POST /upload-soundfont` success handler: calls `invalidate_catalog_cache()` + triggers a background task (FastAPI `BackgroundTasks`) to re-scan. Does NOT block HTTP response.
### A4 — JS API wrapper
- In `app/static/js/services/api.js`, add `SonicAPI.getSoundfontCatalog()``GET /api/v1/plugins/soundfonts/catalog`.
---
## Task B: DecentSampler + Pianobook Support
### B1 — Dockerfile updates
- Add `libcurl4` to `apt-get install`.
- Pre-create `/opt/daw_engine/vst3/` and `/opt/daw_engine/samples/pianobook/` with `mkdir -p`.
### B2 — Host dirs
- Create `vst_plugins/` and `samples/pianobook/` at repo root. Add both to `.gitignore`.
### B3 — DecentSamplerManager in `app/core/vst_engine.py`
- `create_decent_sampler_instance(dspreset_path)`:
- Resolve to absolute path with `os.path.abspath()`.
- Save original CWD with `os.getcwd()`, then `os.chdir()` to `.dspreset` parent dir before `load_preset()` (Refinement 4).
- Restore original CWD in `finally` block.
- Return `VST3Plugin` instance ready for rendering.
### B4 — Wire into `app/core/render_engine.py`
- If track selects a Pianobook instrument (e.g. `instrument_source: "pianobook"`), route through `DecentSamplerManager` instead of FluidSynth or synth fallback.
- The Pianobook path uses `pedalboard.Pedalboard([vst])` with MIDI messages, same as other VST3 paths.
---
## Task C: AI Tool Schema & Prompt Injection
### C1 — Update `generate_multitrack_midi` tool in `aiGateway.js`
- Add to `parameters.properties.tracks.items.properties`:
- `soundfont_id`: `{ type: "string", description: "ID of the SoundFont file (e.g. 'generaluser_gs')" }`
- `soundfont_bank`: `{ type: "integer", default: 0, description: "MIDI Bank. 0 = melodic, 128 = drums/percussion" }`
- `soundfont_program`: `{ type: "integer", description: "MIDI Program number 0-127 from instrument catalog" }`
- Add all 3 to `required` array.
### C2 — Inject condensed catalog into system instruction
- Modify `buildUserMessage()` in `aiGateway.js`:
- When `systemInstruction` is empty and global `window.__soundfontCatalog` exists, prepend a `system` message block containing the condensed catalog text.
- Format: one line per GM category with bank/program examples.
- **Enforce bank rule** (per 35.1_VST_FIX.md §3.B): Add explicit instruction — _`soundfont_bank: 0` for all melodic instruments, `soundfont_bank: 128` for Drum Kits._
### C3 — Fetch catalog on frontend startup
- In `app/static/js/app.jsx`, after auth check, call `SonicAPI.getSoundfontCatalog()`.
- Store result in `window.__soundfontCatalog = { condensed_catalog, full_catalog }`.
- Re-fetch after any SF2 upload succeeds.
---
## Task D: Server Render — Program Change & Channel Mapping
### D1 — Read bank/program from track metadata
- In `render_engine.py` MIDI rendering block, extract `soundfont_bank` and `soundfont_program` from track dict.
- Default: bank=0, program=0.
### D2 — MIDI channel routing + FluidSynth update
- **Channel rules** (per 35.1_VST_FIX.md §5.B):
- `bank == 128` or track has `is_percussion: true``midi_channel = 9` (GM channel 10, percussion).
- Otherwise → assign channels sequentially from 08, one per unique percussion-group track.
- Replace hardcoded `fl.program_select(0, fid, 0, 0)` with:
```python
midi_channel = 9 if (bank == 128 or track.get("is_percussion")) else channel_counter
fl.program_select(midi_channel, fid, bank, prog)
```
- All note_on/note_off events for that track must use the same `midi_channel`.
### D3 — VST3/Pedalboard path: CC + PC insertion
- Extend `midi_events_to_messages()` or add a wrapper that inserts two MIDI messages at sample_offset=0 before note messages:
- `MidiMessage(control_change=0, value=bank, sample_offset=0)` — CONTROL_CHANGE CC 0 (Bank Select MSB)
- `MidiMessage(program_change=program, sample_offset=0)` — PROGRAM_CHANGE
- These are prepended to the message list before `Pedalboard([vst])` processes the buffer.
---
## Task E: Client SoundFont Player — Program Change & Channel Allocation
### E1 — Add channel-state tracking to `soundfontPlayer.js`
- Add internal `_channels` array (size 16), each entry: `{ bank: 0, program: 0 }`.
- `controllerChange(channel, controller, value)`:
- If `controller === 0` (Bank Select MSB), store `bank` for that channel.
- `programChange(channel, program)`:
- Store `program` for that channel.
- Modify `playNote()` to accept an optional `channel` parameter and use the stored bank/program for ADSR/osc-type selection.
### E2 — Add `applyAITrackInstrument(trackId, bank, program)`
- New function in `soundfontPlayer.js`:
- Determine MIDI channel: `bank === 128 || isPercussion ? 9 : track_index % 9`.
- Call `controllerChange(channel, 0, bank)`.
- Call `programChange(channel, program)`.
- Called from `app.jsx` after AI returns `generate_multitrack_midi` with track instrument data.
### E3 — Wire into post-AI pipeline in `app.jsx`
- In the DAW command dispatch loop (around line 12853), after processing `generate_multitrack_midi` function call:
- For each returned track with `soundfont_bank`/`soundfont_program`, call `applyAITrackInstrument()`.
- Log the action to `aiActionLog`.
---
## Task F: Background Cache Invalidation on Upload
### F1 — Update `POST /upload-soundfont` in `plugins.py`
- After saving the uploaded SF2 file:
1. Call `SoundFontInspector.invalidate_catalog_cache()`.
2. Use FastAPI `BackgroundTasks` to queue a re-scan: `background_tasks.add_task(generate_full_catalog)`.
3. Return HTTP 200 immediately (not block on scan).
### F2 — Frontend catalog re-fetch after upload
- In `app.jsx` upload handler, after `SonicAPI.uploadSoundFont()` succeeds, call `SonicAPI.getSoundfontCatalog()` and update `window.__soundfontCatalog`.
---
## Task G: Validation
### G1 — Catalog API
- `GET /api/v1/plugins/soundfonts/catalog` → valid JSON with `{ full_catalog: {...}, condensed_catalog: {...} }`.
- Condensed catalog contains ≤50 entries, grouped by GM category.
### G2 — AI generation
- Input: _"Compose 8 bars of Brass horns and a drum kit"_
- Verify AI returns `generate_multitrack_midi` call with:
- Brass track: `program: 56`, `bank: 0`, `soundfont_id: "generaluser_gs"`.
- Drums track: `program: 0`, `bank: 128`, `soundfont_id: "generaluser_gs"`.
### G3 — Client instrument switching
- After AI response, verify `applyAITrackInstrument` is called with correct bank/program per track.
- Verify MIDI channel allocation: melodic → ch0-8, drums → ch9.
- Verify `controllerChange(CC0)` + `programChange()` dispatched per channel.
### G4 — Server render
- Export WAV, verify correct Brass horn and Drum sound.
- For FluidSynth path: confirm `program_select` uses correct channel, bank, program.
- For VST3 path: confirm CC0 + PC inserted before notes.
### G5 — Upload cache invalidation
- Upload a new `.sf2` file → verify `catalog` endpoint updates without manual restart.
- Upload a corrupted `.sf2` file → verify it is skipped gracefully (Refinement 5).
### G6 — Regression
- `pytest tests/` passes with no regressions.
---
## Implementation Order
1. **A1A4** (sf2utils + soundfont_inspector + catalog API + JS wrapper) — foundational.
2. **C1C3** (AI tool schema + condensed prompt injection + startup fetch) — depends on A3/A4.
3. **D1D3** (server render program change + channel mapping + CC/PC insertion) — depends on C1 for field names.
4. **F1F2** (background cache invalidation on upload) — depends on A3.
5. **E1E3** (client program change + channel allocation + post-AI wiring) — independent of D, but shares channel routing logic.
6. **B1B4** (DecentSampler) — last, requires manual VST3 binary download.
7. **G1G6** (validation).
+757 -325
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+19 -9
View File
@@ -53,11 +53,11 @@
// Default settings
let oscType = 'triangle';
let attackTime = 0.01;
let attackTime = 0.03;
let decayTime = 0.1;
let sustainLevel = 0.5;
let releaseTime = 0.2;
let volFactor = 0.3;
let volFactor = 0.25;
const prog = program !== undefined ? parseInt(program) : 0;
if (prog >= 0 && prog <= 7) { // Pianos
@@ -125,7 +125,7 @@
}
osc.type = oscType;
osc.frequency.value = freq;
osc.frequency.setValueAtTime(freq, 0);
const startAt = startTime !== undefined ? startTime : ctx.currentTime;
const durSec = durationMs / 1000;
@@ -139,8 +139,8 @@
noteGain.gain.linearRampToValueAtTime(targetGain * sustainLevel, startAt + attackTime + decayTime);
const releaseStart = startAt + Math.max(attackTime + decayTime, durSec);
noteGain.gain.setValueAtTime(targetGain * sustainLevel, releaseStart);
noteGain.gain.exponentialRampToValueAtTime(0.001, releaseStart + releaseTime);
noteGain.gain.linearRampToValueAtTime(targetGain * sustainLevel, releaseStart);
noteGain.gain.linearRampToValueAtTime(0, releaseStart + releaseTime);
osc.connect(noteGain);
@@ -149,11 +149,11 @@
osc.start(startAt);
const stopAt = releaseStart + releaseTime + 0.05;
const stopAt = releaseStart + releaseTime + 0.02;
osc.stop(stopAt);
const oscId = `${note}_${Date.now()}_${Math.random()}`;
activeOscillators[oscId] = osc;
activeOscillators[oscId] = { osc, gain: noteGain };
// Clean up active oscillator reference after it stops
setTimeout(() => {
@@ -164,8 +164,18 @@
},
stopAll: function () {
Object.values(activeOscillators).forEach(osc => {
try { osc.stop(); } catch (e) { }
const ctx = getCtx();
const now = ctx.currentTime;
Object.values(activeOscillators).forEach(entry => {
try {
if (entry.gain) {
entry.gain.gain.cancelScheduledValues(now);
entry.gain.gain.setValueAtTime(0, now);
}
if (entry.osc) {
try { entry.osc.stop(now); } catch (e) { }
}
} catch (e) { }
});
Object.keys(activeOscillators).forEach(k => delete activeOscillators[k]);
},
Binary file not shown.
+344
View File
@@ -0,0 +1,344 @@
# TECHNICAL PLAN: DECENT SAMPLER + PIANOBOOK INSTALLATION AND SOUNDFONT MAPPING EXTRACTION FOR AI AGENT
---
## 1. Executive Overview
The system needs to fulfill two core requirements:
* **Integrate DecentSampler & Pianobook on Linux Server:**
* Install `DecentSampler.vst3` (Linux 64-bit) into the Docker Server environment.
* Structure the Pianobook sample library directories (`.dspreset` + `.wav` files).
* Integrate Preset Loading into Python `pedalboard` for offline rendering steps.
* **Resolve the AI Agent's "Instrument Information Blindness" regarding SoundFont (`.sf2`):**
* *Current State:* The AI Agent and the system only load raw `.sf2` files without knowing what instruments are contained within (which Bank, which Program/Patch number, or what the instrument names are).
* *Solution:*
* Build a **SoundFont Inspection Engine** using Python (`sf2utils`) to scan `.sf2` files upon upload/scan and extract the instrument catalog table (Bank, Program/Preset ID, Instrument Name).
* Export a Catalog table (`soundfont_catalog.json`) and pass this context to the AI Agent.
* Update the AI Tool Schema so the AI accurately passes `soundfont_id`, `bank`, and `program` (MIDI Program Change) when creating a Track.
---
## 2. DecentSampler + Pianobook Installation Plan (Server Backend)
### 2.1 Installing DecentSampler Linux Native VST3 in Docker
1. **Download DecentSampler Linux VST3:**
* Download the official Linux 64-bit build from the DecentSampler website (`DecentSampler_Linux_x64.tar.gz` or `.vst3` file).
2. **Server Directory Structure:**
```text
/opt/daw_engine/
├── vst3/
│ └── DecentSampler.vst3/ <-- Native Linux VST3 Binary
├── soundfonts/
│ ├── GeneralUser_GS.sf2
│ └── SGM-V2.01.sf2
└── samples/
└── pianobook/ <-- Pianobook Sample Libraries
├── salamander_grand_piano/
│ ├── salamander_piano.dspreset
│ └── samples/ (*.wav)
└── acoustic_guitar/
├── guitar.dspreset
└── samples/ (*.wav)
```
3. **Additions to `Dockerfile`:**
```dockerfile
# Install audio system dependencies
RUN apt-get update && apt-get install -y \
libgl1-mesa-glx \
libfreetype6 \
libcurl4 \
&& rm -rf /var/lib/apt/lists/*
# Copy DecentSampler VST3 to Server
COPY ./vst_plugins/DecentSampler.vst3 /opt/daw_engine/vst3/DecentSampler.vst3
```
### 2.2 Integrating DecentSampler into the Python Engine (`app/core/vst_engine.py`)
The `pedalboard` library supports loading VST3 plugins and preset files for DecentSampler:
```python
import os
from pedalboard import VST3Plugin
class DecentSamplerManager:
def __init__(self, vst_path="/opt/daw_engine/vst3/DecentSampler.vst3"):
self.vst_path = vst_path
def create_decent_sampler_instance(self, dspreset_path: str) -> VST3Plugin:
"""
Instantiates VST3 DecentSampler and loads the Pianobook sample preset (.dspreset) file.
"""
if not os.path.exists(self.vst_path):
raise FileNotFoundError(f"DecentSampler VST3 not found at {self.vst_path}")
plugin = VST3Plugin(self.vst_path)
# Load the Pianobook preset file into DecentSampler VST3
if os.path.exists(dspreset_path):
plugin.load_preset(dspreset_path)
return plugin
```
---
## 3. Designing the SoundFont Inspection Engine (Bank/Program Extraction)
Every SoundFont (`.sf2`) file is a collection of Presets (or Programs). For the AI Agent to know what sound presets exist inside the `.sf2` file, the Backend must scan and parse the `.sf2` file.
### 3.1 Adding Python SoundFont Inspection Libraries
Add to `requirements.txt`:
```text
sf2utils>=0.9.0
mido>=1.3.0
```
### 3.2 Building the SoundFont Metadata Inspection Service (`app/core/soundfont_inspector.py`)
```python
import os
import json
from sf2utils.sf2parse import Sf2File
class SoundFontInspector:
def __init__(self, sf_dir="/opt/daw_engine/soundfonts"):
self.sf_dir = sf_dir
def inspect_sf2_file(self, filepath: str) -> dict:
"""
Parses an .sf2 file and returns a complete instrument catalog (Bank, Program, Instrument Name).
"""
if not os.path.exists(filepath):
return {}
sf_name = os.path.basename(filepath)
sf_id = os.path.splitext(sf_name)[0].lower()
instruments = []
with open(filepath, 'rb') as f:
sf2 = Sf2File(f)
for preset in sf2.presets:
# Ignore EOP (End of Header) preset
if preset.name.strip() == "EOP" or (preset.bank == 128 and preset.preset == 127):
continue
instruments.append({
"bank": preset.bank, # Bank number (0 = General MIDI Standard, 128 = Percussion/Drums)
"program": preset.preset, # Program/Patch number (0-127)
"name": preset.name.strip(), # Instrument name (e.g. "Stereo Grand", "Violin", "Brass Section")
"is_percussion": (preset.bank == 128)
})
return {
"soundfont_id": sf_id,
"filename": sf_name,
"total_instruments": len(instruments),
"instruments": instruments
}
def generate_full_catalog(self, output_json_path="/opt/daw_engine/soundfont_catalog.json"):
"""
Scans all .sf2 files in the directory and builds a Catalog JSON for the AI Agent.
"""
catalog = {}
for root, dirs, files in os.walk(self.sf_dir):
for file in files:
if file.endswith(('.sf2', '.SF2')):
full_path = os.path.join(root, file)
sf_info = self.inspect_sf2_file(full_path)
catalog[sf_info["soundfont_id"]] = sf_info
with open(output_json_path, 'w', encoding='utf-8') as f:
json.dump(catalog, f, ensure_ascii=False, indent=2)
return catalog
```
### 3.3 Catalog File Output Structure (`soundfont_catalog.json`)
This JSON file serves as an Instrument Dictionary for the AI Agent:
```json
{
"generaluser_gs": {
"soundfont_id": "generaluser_gs",
"filename": "GeneralUser_GS.sf2",
"total_instruments": 128,
"instruments": [
{ "bank": 0, "program": 0, "name": "Stereo Grand Piano", "is_percussion": false },
{ "bank": 0, "program": 19, "name": "Church Organ", "is_percussion": false },
{ "bank": 0, "program": 40, "name": "Violin Ensemble", "is_percussion": false },
{ "bank": 0, "program": 56, "name": "Trumpet", "is_percussion": false },
{ "bank": 128, "program": 0, "name": "Standard Drum Kit", "is_percussion": true }
]
}
}
```
---
## 4. Guiding the AI Agent in Instrument Selection & Accurate Note Loading
When the user types: *"Create a Piano track and a Strings section track for 8 bars"*, the AI Agent needs to know precisely which soundfont, bank, and program numbers to assign to the Tracks.
### 4.1 Updating the AI Tool Function Spec (`tools_spec.json`)
Add `soundfont_bank` and `soundfont_program` fields to the Tool Schema sent to the AI:
```json
{
"type": "function",
"function": {
"name": "generate_multitrack_midi",
"description": "Generates multi-track MIDI data along with appropriate SoundFont Program configurations.",
"parameters": {
"type": "object",
"properties": {
"tracks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"track_name": { "type": "string" },
"instrument_type": { "type": "string", "enum": ["PIANO", "STRINGS", "BRASS", "SYNTH", "DRUMS"] },
"soundfont_id": {
"type": "string",
"description": "ID of the SoundFont to use (e.g. 'generaluser_gs')"
},
"soundfont_bank": {
"type": "integer",
"default": 0,
"description": "MIDI Bank code (0 for melodic instruments, 128 for Drums)"
},
"soundfont_program": {
"type": "integer",
"description": "MIDI Program Number (0-127) corresponding to the instrument name in the Catalog"
},
"notes": { "type": "array", "items": { "type": "object" } }
},
"required": ["track_name", "soundfont_id", "soundfont_bank", "soundfont_program", "notes"]
}
}
}
}
}
}
```
### 4.2 Injecting the Catalog into the AI Agent's Context Prompt (Prompt Template)
Before sending the user query to the LLM, the system reads `soundfont_catalog.json` and injects a condensed catalog table into the System Instruction:
```python
# System Context Prompt Injector
def build_ai_system_instruction(catalog_data: dict) -> str:
sf_summary = []
for sf_id, sf_info in catalog_data.items():
sf_summary.append(f"SoundFont ID: '{sf_id}' (File: {sf_info['filename']}):")
for inst in sf_info['instruments'][:20]: # Inject primary instrument lists
sf_summary.append(
f" - [{inst['name']}]: bank={inst['bank']}, program={inst['program']}"
)
catalog_context = "\n".join(sf_summary)
system_instruction = f"""
You are an AI Copilot for a DAW. Below is the Catalog of available SoundFonts on the system:
{catalog_context}
MANDATORY RULES WHEN CREATING TRACKS:
1. When creating any track, you MUST look up the catalog above and fill in the correct `soundfont_id`, `soundfont_bank`, and `soundfont_program`.
2. Example: If the user requests "Piano", select soundfont_id="generaluser_gs", soundfont_bank=0, soundfont_program=0 ("Stereo Grand Piano").
3. If the user requests "Violin/Strings", select soundfont_bank=0, soundfont_program=40 ("Violin Ensemble").
4. If the user requests "Drums", select soundfont_bank=128, soundfont_program=0 ("Standard Drum Kit").
"""
return system_instruction
```
### 4.3 Applying Program Change on Client & Server Render Layers
#### A. Client Browser Side (FluidSynth Wasm / SoundFont Player)
Upon receiving JSON from the AI, the Frontend invokes the Bank and Program selection function to trigger the correct sound:
```javascript
// Client-side Javascript (spessasynth / fluidsynth.wasm)
function applyAITrackInstrument(trackId, soundfontBank, soundfontProgram) {
const channel = getTrackMIDIChannel(trackId);
// Send MIDI Bank Select (CC 0)
synthInstance.controllerChange(channel, 0, soundfontBank);
// Send MIDI Program Change
synthInstance.programChange(channel, soundfontProgram);
}
```
#### B. Server Offline Render Side (`app/core/render_engine.py`)
When rendering to a WAV file, Python inserts a MIDI Program Change event ahead of the Track's note sequence:
```python
import mido
def create_midi_track_with_program(notes_data, bank=0, program=0):
midi_track = mido.MidiTrack()
# 1. Insert Bank Select (Control Change 0)
midi_track.append(mido.Message('control_change', channel=0, control=0, value=bank, time=0))
# 2. Insert Program Change (Instrument Sound Selection)
midi_track.append(mido.Message('program_change', channel=0, program=program, time=0))
# 3. Insert MIDI notes generated by AI
for note in notes_data:
start_tick = int(note['start_beat'] * 480) # 480 ticks per beat
dur_tick = int(note['duration_beats'] * 480)
pitch = int(note['pitch'])
vel = int(note['velocity'] * 127)
midi_track.append(mido.Message('note_on', note=pitch, velocity=vel, time=start_tick))
midi_track.append(mido.Message('note_off', note=pitch, velocity=0, time=dur_tick))
return midi_track
```
---
## 5. Action Checklist
* [ ] **Step 1:** Download Linux 64-bit `DecentSampler.vst3` and copy it into `/opt/daw_engine/vst3/`.
* [ ] **Step 2:** Download Pianobook sound libraries (e.g. Salamander Grand Piano) and extract them to `/opt/daw_engine/samples/pianobook/`.
* [ ] **Step 3:** Add `sf2utils` to `requirements.txt` and install it in Docker.
* [ ] **Step 4:** Create `app/core/soundfont_inspector.py` to automatically scan all `.sf2` files in the project and generate `soundfont_catalog.json`.
* [ ] **Step 5:** Build API Endpoint `GET /api/v1/plugins/soundfonts/catalog` returning the extracted instrument catalog.
* [ ] **Step 6:** Update the Prompt Template and AI Tool Schema to support `soundfont_bank` & `soundfont_program`.
* [ ] **Step 7:** End-to-End Verification: Type Prompt *"Generate 8 bars of Brass horn music"* $\rightarrow$ AI reads Catalog and selects Program `56` $\rightarrow$ Web Audio & Server Render produce the correct Brass horn instrument sound.
+115
View File
@@ -0,0 +1,115 @@
# INTEGRATION & OPERATIONAL GUIDE: SOUNDFONT & VST3 ENGINE SYSTEM
This document outlines the workflow for connecting and operating the designed technical methods and modules across the entire DAW system, clearly categorized by system integration steps.
---
## 1. System Environment & Storage Setup
### A. Server & Docker Directory Structure
* **System SoundFont Directory (`/opt/daw_engine/soundfonts/`):** Stores system default `.sf2` files (e.g., `GeneralUser_GS.sf2`, `SGM-V2.01.sf2`).
* **User Upload Directory (`app/storage/uploads/soundfonts/`):** Stores `.sf2` files uploaded by users via the web interface.
* **VST3 & Pianobook Directories (`/opt/daw_engine/vst3/`, `/opt/daw_engine/samples/pianobook/`):** Contains the `DecentSampler.vst3` binary along with the directory structure holding `.dspreset` sample files and `samples/*.wav` subdirectories.
### B. System Dependencies
Ensure the `Dockerfile`/`Virtualenv` has installed the `libcurl4` system library (mandatory for DecentSampler) and the Python package `sf2utils>=0.9.0`.
---
## 2. SoundFont Catalog Operational Lifecycle
### A. First Startup (Lazy Initialization)
* When the Server boots, the Catalog is not generated immediately to prevent slowing down the app boot time.
* When the Frontend dispatches its first request to the API Endpoint `GET /api/v1/plugins/soundfonts/catalog`, the Backend triggers `SoundFontInspector` to simultaneously scan both system and upload directories.
* The extracted data is categorized into 2 versions:
* **Full Catalog:** Designed for the UI to display the complete list of instruments.
* **Condensed Catalog:** A summary (maximum 4050 representative instruments categorized under General MIDI groups such as Piano, Brass, Drums, etc.) specifically tailored for the AI Agent.
* The parsed data is cached in memory (Memory Cache) for subsequent queries.
### B. Cache Invalidation on User Upload
* Once the upload handling endpoint successfully saves an uploaded `.sf2` file to the upload directory:
* Automatically invokes the `invalidate_catalog_cache()` method to purge the memory cache.
* Triggers a Background Task calling the catalog initialization function to incrementally scan the new file without blocking the user's HTTP response.
---
## 3. AI Copilot Integration Workflow (AI Gateway & System Prompt)
### A. Initial Instrument Catalog Load (Frontend Startup)
* As soon as the Web application launches (`app.jsx`), the Frontend proactively calls the API to fetch the Catalog.
* Extracts the `condensed_catalog` section and persists it into the application's global state (Global State).
### B. Automated Prompt Context Injection
* When a user submits an interaction command to the AI:
* The System Instruction generator reads the `condensed_catalog` and converts it into a concise text description of available instruments (including name, bank code, and program code).
* Enforces the rule that the AI must assign `soundfont_bank: 0` for melodic instruments and `soundfont_bank: 128` for Drum Kits.
### C. Function Calling Schema Definition
* When dispatching requests to the LLM, the Tools list configuring `generate_multitrack_midi` includes 3 mandatory fields for every Track: `soundfont_id`, `soundfont_bank`, and `soundfont_program`.
---
## 4. Real-time Client-Side Instrument Switching (Browser Playback)
### A. Listening for AI Responses
* When the AI successfully completes a Function Call and returns a JSON payload containing musical notes alongside `soundfont_bank` & `soundfont_program` parameters for each Track:
* The Client allocates each Track to a corresponding MIDI Channel (Channels 0 through 8 for standard instruments, fixed Channel 9 for Drum Kits).
### B. Applying Real-Time Program Changes
* Calls the `applyAITrackInstrument` method on the Client's SoundFont Player module.
* The module dispatches a Control Change (CC 0) signal to select the Bank, followed by a Program Change event to the designated MIDI channel to immediately play the newly selected instrument sound inside the browser.
---
## 5. Server-Side Offline Render Workflow (Audio Export)
When a user clicks "Export WAV" or "Bounce Track", the processing pipeline on the Server executes as follows:
### A. Reading Track Metadata
Extracts `soundfont_bank` and `soundfont_program` parameters from the Track metadata received in the project's JSON payload.
### B. MIDI Channel Routing & FluidSynth Rendering
* **Channel Rules:** If `soundfont_bank == 128` or the track is marked as percussion (`is_percussion`), rigidly assigns `midi_channel = 9` (Channel 10 under the General MIDI standard). Otherwise, assigns free channels from 0 to 8.
* Executes `program_select` settings on the FluidSynth Instance targeting the correct channel, bank, and program before feeding the note sequence into the audio rendering buffer.
### C. Rendering Pianobook (`.dspreset`)
* If a Track selects a Pianobook instrument source:
* Calls `DecentSamplerManager` passing the absolute file path to the `.dspreset` file.
* The manager automatically changes the Current Working Directory (CWD) temporarily to the parent folder of the `.dspreset` file, loads the preset into VST3, and subsequently restores the original working directory to prevent "Sample Not Found" errors on relative `.wav` sample files.
### D. Rendering VST3 via Pedalboard
Prior to passing the MIDI note array into the VST3 Plugin, inserts 2 initialization MIDI messages at timestamp $0.0\text{s}$:
* A `control_change` message (Control 0, Value = bank).
* A `program_change` message (Program = program).
---
## 6. Verification & Testing Workflow
* **Catalog API Verification:** Use Postman or a browser to call `GET /api/v1/plugins/soundfonts/catalog`, confirming that the returned payload contains both `full_catalog` and `condensed_catalog`.
* **AI Response Verification:** Input the command *"Compose 8 bars of Brass horns and a drum kit"* $\rightarrow$ Inspect the returned JSON from the AI to verify that the Brass track has `program: 56`, `bank: 0` and the Drums track has `program: 0`, `bank: 128`.
* **Audio Output Verification:** Export the WAV file and listen to confirm that the Brass horn and Drum sounds are rendered using the correct instrument patches.
+130
View File
@@ -0,0 +1,130 @@
# ASSESSMENT REPORT & OPTIMIZATION PLAN: LINUX VST3 & SOUNDFONT MAPPING ENGINE
---
## 1. Executive Review
Your plan closely aligns with the current codebase status and correctly identifies key bottlenecks (such as missing `libcurl4`, missing `sf2utils`, hardcoded `program_select(0, fid, 0, 0)` calls in `render_engine.py`, and missing schema fields in the AI Tool Schema).
However, to guarantee stable system operation within the Docker Linux environment and prevent failures during End-to-End execution, the plan requires the 5 critical technical refinements detailed below.
---
## 2. 5 Mandatory Technical Refinements
### 💡 Refinement 1: AI Prompt Context Size Control (Avoiding Token Overflow)
* **Problem in Previous Plan:** Injecting the entire `soundfont_catalog.json` file into the AI System Instruction. A full SoundFont file (like GeneralUser GS or SGM-V2.01) can contain hundreds to thousands of presets/notes, causing LLM Token Limit overflows, inflating costs, and degrading response latency.
* **Solution:**
* Implement `get_condensed_catalog_summary()` within `SoundFontInspector` to extract only a condensed catalog (categorized into core instrument groups: Piano, Organ, Guitar, Bass, Strings, Ensemble, Brass, Reed, Pipe, Synth Lead, Synth Pad, Drum Kit).
* Inject a maximum of 4050 of the most common instruments along with their representative bank and program codes into the AI Prompt.
### 💡 Refinement 2: MIDI Channel Handling for Percussion Kits (Bank 128 / Percussion)
* **Problem in Previous Plan:** Defaulting to `channel=0` for all tracks when invoking `program_select(0, fid, bank, prog)`. In General MIDI and SoundFont (`.sf2`) standards, Drum/Percussion sounds (Bank 128) must reside on MIDI Channel 9 (the 10th channel, 0-based index 9).
* **Solution:**
* In `render_engine.py`, if `soundfont_bank == 128` or `is_percussion == True`, automatically assign that track's MIDI Channel to `channel = 9` for both FluidSynth rendering and `mido` message generation.
### 3. Refinement 3: Catalog Refresh on User SoundFont Upload (Cache Invalidation)
* **Problem in Previous Plan:** The `GET /api/v1/plugins/soundfonts/catalog` endpoint scans the catalog only once or upon application startup. When a user uploads a new `.sf2` file via `POST /api/v1/audio/upload-soundfont`, the AI remains unaware of the newly added file.
* **Solution:**
* Implement a Cache Invalidation mechanism: Upon successfully saving an uploaded `.sf2` file, automatically invoke `SoundFontInspector.generate_full_catalog()` to update the `soundfont_catalog.json` file.
### 💡 Refinement 4: Handling Relative Sample Paths for Pianobook `.dspreset` Files
* **Problem in Previous Plan:** Pianobook `.dspreset` files contain relative path links pointing to subfolder `samples/*.wav` files. When DecentSampler VST3 loads a `.dspreset` file via `pedalboard`, if the Working Directory is not set to the folder containing the `.dspreset` file, the VST3 engine triggers a "Sample Not Found" error (resulting in silence).
* **Solution:**
* Before invoking `plugin.load_preset(dspreset_path)`, ensure an absolute path (`os.path.abspath(dspreset_path)`) is passed and temporarily switch the Working Directory or properly configure the Root Sample Directory for DecentSampler.
### 💡 Refinement 5: Robust Error Handling in `SoundFontInspector`
* **Problem in Previous Plan:** If a user uploads a corrupted or malformed `.sf2` file, the `sf2utils` library may throw an exception, crashing the entire Catalog scanning workflow.
* **Solution:**
* Wrap each `.sf2` file processing block inside a `try...except` block in `SoundFontInspector`. If a file is corrupted, log a warning and skip that specific file instead of interrupting the complete scan process.
---
## 3. Updated Execution Plan
### Task A: SoundFont Inspection Engine (`sf2utils`)
* [x] **A1:** Add `sf2utils>=0.9.0` to `requirements.txt`.
* [x] **A2:** Create `app/core/soundfont_inspector.py`:
* Add `inspect_sf2_file(filepath)` wrapped in a `try...except` block.
* Add `generate_full_catalog(output_json_path)` scanning both `/opt/daw_engine/soundfonts` and `app/storage/uploads/soundfonts`.
* Add `get_condensed_catalog_summary()` to build a condensed summary for the AI Context Prompt.
* [x] **A3:** Create API Endpoint `GET /api/v1/plugins/soundfonts/catalog` in `app/api/v1/plugins.py`:
* Return Full Catalog for Frontend UI and Condensed Catalog for AI Agent.
* Integrate cache refresh functionality triggered upon new `.sf2` file uploads.
### Task B: DecentSampler + Pianobook Support
* [x] **B1:** Update `Dockerfile`:
* Add `libcurl4` to the `apt-get install` package list.
* Pre-create directory structures `/opt/daw_engine/vst3/` and `/opt/daw_engine/samples/pianobook/`.
* [x] **B2:** Create local host directory structure `vst_plugins/` and `samples/pianobook/` (Update `.gitignore`).
* [x] **B3:** Update `app/core/vst_engine.py`:
* Add `DecentSamplerManager` supporting `.dspreset` loading using absolute paths.
* [x] **B4:** Integrate Pianobook rendering into `app/core/render_engine.py` when a Track selects a Pianobook instrument.
### Task C: AI Tool Schema & Prompt Injection
* [x] **C1:** Update `DEFAULT_TOOLS` in `app/static/js/services/aiGateway.js`:
* Add 3 properties to the `generate_multitrack_midi` schema: `soundfont_id` (string), `soundfont_bank` (integer), `soundfont_program` (integer).
* [x] **C2:** Inject condensed instrument catalog into System Instruction within `aiGateway.js`.
* [x] **C3:** Load Catalog automatically upon Frontend application startup (`app.jsx`).
### Task D: Server Render — Program Change & Channel Mapping
* [x] **D1:** Update `render_engine.py`:
* Read `soundfont_bank` and `soundfont_program` from Track metadata.
* MIDI channel rules: If `soundfont_bank == 128` (Drums), automatically assign `channel = 9` (GM Standard Channel 10). Otherwise, assign channels from 0 through 8.
* [x] **D2:** Update FluidSynth render path:
```python
midi_channel = 9 if (bank == 128 or track.get("is_percussion")) else target_channel
fl.program_select(midi_channel, fid, bank, prog)
```
* [x] **D3:** Update VST3/Pedalboard render path:
* Insert `CONTROL_CHANGE` (CC 0 for Bank) and `PROGRAM_CHANGE` events into the note sequence prior to rendering the audio buffer.
### Task E: Client SoundFont Player — Program Change
* [x] **E1:** Update `app/static/js/services/soundfontPlayer.js`:
* Add `programChange(channel, program)` and `controllerChange(channel, controller, value)` methods.
* [x] **E2:** Add `applyAITrackInstrument(trackId, bank, program)` function to dynamically switch instrument sounds in real time when AI generates new Tracks on the UI.
### Task F: Validation & Testing
* [x] **F1:** Test Catalog API: `GET /api/v1/plugins/soundfonts/catalog`.
* [x] **F2:** Test AI Generation: Input prompt *"Compose 8 bars of Brass horns and a drum kit"* $\rightarrow$ Verify AI returns JSON with `program=56` (Brass) and `bank=128` (Drums).
* [x] **F3:** Test Server Render: Export WAV $\rightarrow$ Listen to output audio file to verify correct Brass horn and Drum sound execution.
+131
View File
@@ -8,6 +8,30 @@
- **Tóm tắt thay đổi:** (1) Piano Roll zoom-out không còn màn hình đen — bars fill toàn bộ viewport. (2) Shift+scroll trong Piano Roll di chuyển playhead và play notes MIDI như fast-forward. (3) Khi drag section/MIDI/clip đến cạnh phải timeline, auto-scroll container. (4) Server-side cache FluidSynth instances + PluginManager singleton + list_soundfont_instruments cache.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/core/vst_engine.py`
- **Ghi chú/Test (nếu có):** `npm run build` pass. `vst_engine.py` thêm `load_soundfont_cached`, `release_soundfont`, `get_plugin_manager`, `_SF_INSTRUMENTS_CACHE` — refcount-based cache.
---
### [2026-07-26 09:36] Task: Piano Roll 4 features (SNAP, velocity selected, auto-scroll, Synth button)
- **Tóm tắt thay đổi:** (1) SNAP trong MIDI tab — grid snap cho note drawing, selection marquee, loop range. (2) Ctrl+drag velocity — selected notes màu xanh dương, affect only selected; no selection = paint all. (3) Auto-scroll Piano Roll khi brush-drag gần cạnh top/bottom. (4) Synth button trong MIDI tab toolbar — mở instrument selector khi nhấn. Layout fix: thêm `h-full` cho outer div.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass. Fix thiếu 1 `)` tại line 5959 return statement (gây build error pre-existing).
---
### [2026-07-26 10:08] Task: Fix auto-scroll continuous + CC velocity batch + flicker
- **Tóm tắt thay đổi:** (1) Brush-draw gần cạnh top/bottom giờ dùng `setInterval` 30ms để cuộn liên tục (trước đây cuộn 1 lần per mousemove). Stop interval khi mouseup hoặc chuột rời khỏi threshold. (2) Ctrl+drag velocity cho selected notes — gộp tất cả `setNotes` vào 1 batch call (thay vì 1 call per note), tránh flicker do multi re-render. (3) `handleCCMouseDown` selectedMode cũng batch `setNotes`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass. Thêm ref `brushAutoScrollRef` cho setInterval auto-scroll. Batch `setNotes` trong cả `handleCCMouseDown``handleCCMouseMove` để tránh flicker.
---
### [2026-07-26 10:12] Task: Fix brush pitch-based scroll + CC velocity per-note paint
- **Tóm tắt thay đổi:** (1) Brush auto-scroll: chuyển từ scroll theo viewport edge (clientY) sang pitch-based — scroll khi note mới được vẽ ở pitch gần rìa visible area (6 note threshold). Dùng `NoteHeight * 2` scroll step, setInterval 30ms. (2) CC velocity selected mode: thay vì paint all selected notes cùng lúc, paint từng note một theo vị trí chuột — tìm note gần cursor beat nhất, chỉ paint note đó nếu đang được selected.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass. Thay `lastPainted` trong selectedMode từ array all indices thành array single-node index.
---
### [2026-07-26 10:20] Task: Fix brush scroll speed + CC snap + non-selected flicker
- **Tóm tắt thay đổi:** (1) Giảm auto-scroll step từ `NoteHeight * 2` xuống `max(1, floor(NoteHeight * 0.5))` — cuộn chậm hơn để draw note đúng pitch. (2) CC lane `handleCCMouseDown``handleCCMouseMove` dùng `getSnapBeat(beat, snapVal)` khi tìm note index — snap đúng grid. (3) Non-selected CC flicker: xóa `paintNote` helper, inline `setNotes` trực tiếp trong mousedown, không gọi setNotes riêng lẻ gây re-render dư thừa.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-25 07:25] Task: Fix auto-scroll + maxDuration tab isolation + 1-bar margin
- **Tóm tắt thay đổi:** (1) `maxDuration` dùng `activeTracks` + 4-bar buffer. (2) Cách ly MAIN vs SECTION-TAB. (3) Clip/section/MIDI drag/stretch/resize clamp 1-bar from right. (4) Clip drag + stretched clip dùng `updateActiveTracks`. (5) Stretched clip handler thêm auto-scroll.
@@ -139,3 +163,110 @@
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
---
### [2026-07-25 22:05] Task: Fix piano roll selection, playback, marquee drag/resize/move
- **Tóm tắt thay đổi:** (1) Fix playback duration — min 16 beats when no notes exist. (2) Selection marquee: green transparent (#22c55e), full-height from top to CC lane. (3) Document-level mousemove/mouseup for marquee drag (không dừng khi ra khỏi grid). (4) Edge resize handles (left/right) cho marquee. (5) Body drag (move) cho marquee. Hiển thị marquee trên cả grid canvas và CC canvas.
- **Các file ảnh hưởng:** `app/static/js/app.precompiled.js`
- **Ghi chú/Test (nếu có):** `node --check app/static/js/app.precompiled.js` — syntax OK.
---
### [2026-07-25 22:18] Task: Fix marquee mouseleave, loop reschedule, tempo drag
- **Tóm tắt thay đổi:** (1) Separate onMouseLeave from onMouseUp for grid canvas — preserve marquee when mouse leaves canvas. (2) Add MIDI note rescheduling on piano roll loop restart — 2nd+ loop iteration now plays sound. (3) Tempo bar drag already uses document-level listeners — works across columns.
- **Các file ảnh hưởng:** `app/static/js/app.precompiled.js`
---
### [2026-07-26 10:24] Task: Fix scroll speed match mouse + CC flicker useLayoutEffect
- **Tóm tắt thay đổi:** (1) Brush auto-scroll: thêm direct scroll target — mỗi mousemove tính pitchPixel, nếu gần rìa visible area (2 note margin) thì scroll trực tiếp tới target để tốc độ cuộn = tốc độ chuột. Giữ interval 16ms fallback cho trường hợp chuột ở rìa viewport. (2) CC flicker: đổi `React.useEffect``React.useLayoutEffect` cho CC canvas rendering effect — canvas paint trước browser paint, loại bỏ flash khi state thay đổi.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass. Direct scroll target: `container.scrollTop = target` trên mỗi mousemove khi pitch ở gần rìa.
---
### [2026-07-26 10:28] Task: Fix velocity flicker - grid canvas useLayoutEffect
- **Tóm tắt thay đổi:** Đổi grid canvas effect từ `React.useEffect``React.useLayoutEffect`. Nguyên nhân flicker: khi velocity thay đổi, grid canvas (useEffect) vẽ async sau paint → hiển thị velocity fill cũ 1 frame, trong khi CC canvas (useLayoutEffect) đã vẽ sync trước paint → hiển thị stem mới. Sự mismatch giữa 2 canvas tạo flicker. Fix: cả grid + CC canvas đều dùng useLayoutEffect, đồng bộ trước paint.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 10:39] Task: Fix brush shape draw + remove transport + loop repeat sound
- **Tóm tắt thay đổi:** (1) Brush drawing: thay vì redistribute notes evenly, append từng note mới tại beat position thực tế (`getSnapBeat(beat, snapVal)`) — notes theo đúng hình dạng brush stroke. (2) Xóa transport controls (back/play/stop/record/forward) khỏi MIDI tab toolbar — chỉ dùng global transport. (3) Loop repeat: thêm `schedulePianoRollMidi` helper — reschedule MIDI notes qua SonicSF mỗi khi loop repeat, fix lỗi notes trong vùng chọn không có âm thanh khi play lặp lại.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 10:44] Task: Fix brush drawing smooth - track pitch + beat delta
- **Tóm tắt thay đổi:** Brush drawing: theo dõi cả `lastDrawBeat``visitedPitches`. Tạo note mới khi pitch thay đổi HOẶC beat di chuyển >= minBeatStep (25% của grid duration). Dùng raw `beat` thay vì `getSnapBeat` để note theo sát vị trí chuột. Khởi tạo `lastDrawBeat: start` trong draggedNote. Fix lỗi draw bị ngắt quãng do chỉ tạo note khi pitch thay đổi.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 10:47] Task: Fix brush draw only pitch change + CC velocity flicker skip
- **Tóm tắt thay đổi:** (1) Brush draw: chỉ tạo note mới khi `pitchChanged` (pitch chưa visited). Nếu cùng pitch, extend `duration_beats` của note cuối cùng trong `brushIds` — không tạo note mới trùng pitch. (2) CC velocity: thêm check `Math.abs(currentVal - val) > 0.001` trước mỗi `setNotes` — skip render khi velocity/pan không thay đổi, loại bỏ flicker cho cả selected + non-selected mode.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass. currentVal tính: velocity = note.velocity || 0.8; pan = (note.pan || 0) / 2 + 0.5.
### [2026-07-26 10:49] Task: Fix brush draw per-note duration + noteStartBeats tracking
- **Tóm tắt thay đổi:** (1) Thêm `noteStartBeats` array trong `draggedNote` — track start beat của từng note. Khi tạo note mới (pitchChanged), fix duration của note trước đó thành `beat - prevNoteBeat`. Khi extend (cùng pitch), chỉ extend note cuối với `beat - lastNoteBeat` (không extend toàn bộ stroke). (2) Khởi tạo `noteStartBeats: [start]` trong setDraggedNote. Fix lỗi note cuối có duration dài bằng toàn bộ brush stroke.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 10:55] Task: Fix brush draw pitch revisit by comparing lastDrawnPitch
- **Tóm tắt thay đổi:** Thay `!visited.includes(snappedPitch)` bằng `snappedPitch !== lastDrawnPitch`. Bug cũ: khi vẽ C→D→C, visited.includes(C) = true → không tạo note mới → note D cũ bị extend. Fix: so sánh với pitch của note cuối cùng, cho phép vẽ tiếp khi pitch thay đổi dù đã visit trước đó. Bỏ `visitedPitches` (không còn dùng).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 10:58] Task: Fix CC note detection with Y-aware stem matching
- **Tóm tắt thay đổi:** Thêm `findCCNoteIndex(beat, mouseY, ccH)` helper — tìm note trong CC lane dựa trên cả X (beat) và Y (vị trí stem top). Khi nhiều note cùng beat (chord), so sánh `mouseY` với stem top của từng note, chọn note có stem gần nhất. Dùng helper này thay thế tất cả `notes.findIndex` và nearest-fallback trong `handleCCMouseDown``handleCCMouseMove`. Fix lỗi không detect được note khi velocity gần nhau hoặc cùng beat.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 11:03] Task: Remove MIDI tab loop button + Ctrl+click clear range
- **Tóm tắt thay đổi:** (1) Xóa nút loop (repeat) khỏi MIDI tab toolbar — dùng global transport loop button thay thế. (2) Ctrl/Meta+Click trên ruler (tempo track lane) xóa loop range: set loopStartBeat/loopEndBeat = null, selectionStart/selectionEnd = null.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 11:06] Task: Fix MIDI tab real-time instrument change - restart playback
- **Tóm tắt thay đổi:** `setTrackInstrumentWithProgram`: khi đổi instrument, kiểm tra nếu có PIANO_ROLL sub-tab với trackId đang playing → gọi `stopAllPlayback()` + `setTimeout` 50ms để `schedulePianoRollMidi` + `startSubTabPlayback` với instrument mới. Fix lỗi MIDI tab không cập nhật realtime khi thay đổi Synth.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 11:07] Task: Fix instrument change breaks loop playback
- **Tóm tắt thay đổi:** Khi đổi instrument trong lúc MIDI tab đang play loop, restart code giờ cũng reset `startBufferOffsetRef` và gọi `requestAnimationFrame(updatePlayhead)` để tiếp tục vòng lặp animation. Fix lỗi loop bị ngắt sau khi change instrument.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 11:17] Task: Real-time velocity, ruler play, export MIDI button
- **Tóm tắt thay đổi:** (1) Thêm `onRescheduleMidi` prop — khi edit velocity trong CC lane lúc đang play, gọi `window.SonicSF.stopAll()` + `schedulePianoRollMidi` với velocity mới. (2) Click ruler: set `currentTime` + nếu đang play thì `onStop` + `setTimeout` `onPlayPause` để play từ vị trí click. (3) Thêm nút Export cạnh Save — inline MIDI writer, xuất `.mid` file từ notes array.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 11:27] Task: Fix realtime velocity stale notes + ruler click restart
- **Tóm tắt thay đổi:** (1) Compute `updatedNotes` synchronously trước khi gọi `setNotes`, pass into `onRescheduleMidi(updatedNotes)`. `schedulePianoRollMidi` nhận tham số `notesOverride` — dùng notes mới thay vì `st.notes` (stale). (2) Ruler click: thay `onStop()` (reset currentTime) bằng `stopAllPlayback()` trực tiếp + `schedulePianoRollMidi` + `startSubTabPlayback` + RAF từ `clickTime`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 11:32] Task: Fix range auto-loop + gain fade-out crackling
- **Tóm tắt thay đổi:** (1) Xóa `setIsLooping(true)``isLooping: true` khỏi ruler drag handler — range selection không còn tự động loop, chỉ loop khi nút LOOP global được kích hoạt. (2) Thay `window.SonicSF.stopAll()` abrupt bằng gain fade-out: `linearRampToValueAtTime(0.001, +40ms)` → delay 50-60ms → `stopAll` + reschedule → `linearRampToValueAtTime(volLinear, +15ms)`. Áp dụng cho cả `onRescheduleMidi``setTrackInstrumentWithProgram`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 11:35] Task: Fix ruler click playhead - use onStop+onPlayPause props
- **Tóm tắt thay đổi:** Sửa ruler click handler — xóa calls tới `stopAllPlayback`/`schedulePianoRollMidi`/`updatePlayhead` (không có trong scope PianoRollTabEditor). Dùng props: `setSubTabs({currentTime, isPlaying: false})``onStop()` → timeout 40ms → `setSubTabs({currentTime})``onPlayPause()`. Playhead di chuyển và play tiếp từ vị trí click.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 11:40] Task: Fix ruler cursor+range drag + SonicSF smooth stopAll
- **Tóm tắt thay đổi:** (1) Ruler: cursor default, click trong range → drag move (hand cursor → move), click ngoài range → seek/drag tạo selection mới. (2) `soundfontPlayer.js`: `activeOscillators` lưu `{osc, gain}` thay vì `osc`; `stopAll` ramp gain về 0 trong 8ms trước khi `osc.stop(10ms)` — loại bỏ crackling khi realtime update. Cũng xóa `setIsLooping(true)` trong ruler drag cũ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/services/soundfontPlayer.js`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 11:56] Task: Fix Piano Roll playhead seek + sound cracking
- **Tóm tắt thay đổi:** (1) Click ruler khi Piano Roll đang play → seek đến vị trí mới và tiếp tục play (không dừng). (2) ADSR envelope trong `soundfontPlayer.js` dùng `linearRampToValueAtTime` thay `setValueAtTime`/`exponentialRampToValueAtTime` để loại bỏ gain jump gây crackling. `stopAll` ramp gain về 0 trong 20ms trước khi stop oscillator.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.precompiled.js`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 12:20] Task: Fix Piano Roll seek-to-start during playback
- **Tóm tắt thay đổi:** Thêm hàm `seekPlaybackTo(time)` xử lý seek khi đang play cho cả sub-tab và main timeline. Nút "Quay lại đầu" (`skip-back`) và "Đầu vùng chọn" (`step-back`) giờ restart playback từ vị trí mới: stop → update refs → reschedule MIDI → restart.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
- **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 12:13] Task: Fix Piano Roll loop/stop behavior
- **Tóm tắt thay đổi:** (1) Sub-tab loop selection chỉ active khi nút Loop (st.isLooping) bật — không còn bị ảnh hưởng bởi global isLoopingSelection. (2) Stop dừng ngay lập tức: `SonicSF.stopAll()` set gain 0 và stop oscillator tại ctx.currentTime, không ramp.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.precompiled.js`
- **Ghi chú/Test (nếu có):** `npm run build` pass.