Compare commits
67 Commits
8fdf93f5d1
...
f16467eba1
| Author | SHA1 | Date | |
|---|---|---|---|
| f16467eba1 | |||
| b1bbf567ab | |||
| 8204bc016d | |||
| b0d7f35a1e | |||
| 653c0747cb | |||
| 4e880850f5 | |||
| 1ffdec68c6 | |||
| 523a754949 | |||
| 79999065a7 | |||
| 49c9f019db | |||
| e18b261943 | |||
| d3c28d2fa7 | |||
| 7601cc72c9 | |||
| f7b499cda9 | |||
| 5e096b5594 | |||
| 724b42d506 | |||
| d3a3adca8a | |||
| 7ede9bbf72 | |||
| b6b1faf083 | |||
| bf4f2a5f11 | |||
| 90ca8f484b | |||
| 6926c79357 | |||
| 9717470bb6 | |||
| e57f6d96d9 | |||
| 5b500ebdf3 | |||
| 9438f44052 | |||
| 3901af75b2 | |||
| 27d9556a9d | |||
| bc0c2d3279 | |||
| 034455598d | |||
| 8450bd6de4 | |||
| d2993eee69 | |||
| fe379c45e2 | |||
| 3ae48f2f2f | |||
| d20fafb75e | |||
| 0b7dd82407 | |||
| 7107a0cc14 | |||
| 8ec4b05e21 | |||
| 9237d01f35 | |||
| a7417b9bfa | |||
| a765998455 | |||
| bd55d035de | |||
| 8516961d80 | |||
| c917db200e | |||
| e5196e79c8 | |||
| ce30289f46 | |||
| 5bd92ee186 | |||
| 89f2fed1d9 | |||
| 4306ff302a | |||
| 790a5823f6 | |||
| fccdb15ad7 | |||
| a181f7a24d | |||
| 1a5c1eff43 | |||
| 9bfcd1870c | |||
| 39680cb95c | |||
| 2ce731e6a9 | |||
| 025aba3265 | |||
| 57fd110fe3 | |||
| c1a3d3a6dd | |||
| fde9a91c9b | |||
| f2d02a358f | |||
| ce319573ab | |||
| 29b682e3ba | |||
| f4ec769650 | |||
| 966e673fae | |||
| a9f56d0fcd | |||
| 8d83a46e08 |
@@ -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` là `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 40–50 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 0–8, 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. **A1–A4** (sf2utils + soundfont_inspector + catalog API + JS wrapper) — foundational.
|
||||
2. **C1–C3** (AI tool schema + condensed prompt injection + startup fetch) — depends on A3/A4.
|
||||
3. **D1–D3** (server render program change + channel mapping + CC/PC insertion) — depends on C1 for field names.
|
||||
4. **F1–F2** (background cache invalidation on upload) — depends on A3.
|
||||
5. **E1–E3** (client program change + channel allocation + post-AI wiring) — independent of D, but shares channel routing logic.
|
||||
6. **B1–B4** (DecentSampler) — last, requires manual VST3 binary download.
|
||||
7. **G1–G6** (validation).
|
||||
+757
-325
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -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.
@@ -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.
|
||||
@@ -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 40–50 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.
|
||||
@@ -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 40–50 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.
|
||||
@@ -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` và `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` và `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` và `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` và `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)` và `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` và `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.
|
||||
|
||||
Reference in New Issue
Block a user