3 Commits

7 changed files with 1032 additions and 166 deletions
+1
View File
@@ -45,6 +45,7 @@
"id": { "type": "string" },
"name": { "type": "string" },
"type": { "type": "string", "enum": ["AUDIO", "MIDI", "SECTION"] },
"color": { "type": ["string", "null"], "default": null },
"volume_db": { "type": "number", "default": 0.0 },
"pan": { "type": "number", "minimum": -1.0, "maximum": 1.0, "default": 0.0 },
"mute": { "type": "boolean", "default": false },
+651 -130
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+52
View File
@@ -121,6 +121,57 @@ const AIGateway = (function() {
}
};
// ai_midi_rearrange_specification.md §3 — Function Tool Schema hỗ trợ 2 mode:
// SIMILAR_VARIATION (biến tấu cùng độ dài) / EXTEND_CONTINUATION (viết tiếp
// các bar sau). AI trả gói dữ liệu có vị trí target trên Timeline.
const REARRANGE_EXTEND_TOOL_SPEC = {
type: 'function',
function: {
name: 'rearrange_or_extend_midi_melody',
description: 'Analyzes source MIDI melody data and returns either a variation (Variation) or continuation (Extend) based on user instructions.',
parameters: {
type: 'object',
properties: {
mode: {
type: 'string',
enum: ['SIMILAR_VARIATION', 'EXTEND_CONTINUATION'],
description: "Mode: 'SIMILAR_VARIATION' (new arrangement of equal length) or 'EXTEND_CONTINUATION' (writes subsequent bars)."
},
composition_title: {
type: 'string',
description: 'Short title describing the new melody style (e.g., Jazz Swing Variation, Epic Extension Part 2)'
},
target_start_bar: {
type: 'number',
description: 'Starting bar number for the generated notes on the Timeline'
},
target_duration_bars: {
type: 'number',
description: 'Total bar duration covered by the generated sequence'
},
soundfont_id: { type: 'string', default: 'generaluser_gs' },
soundfont_bank: { type: 'integer', default: 0 },
soundfont_program: { type: 'integer', default: 0 },
generated_notes: {
type: 'array',
description: 'Array of AI-generated MIDI notes.',
items: {
type: 'object',
properties: {
pitch: { type: 'integer', minimum: 0, maximum: 127 },
start_beat: { type: 'number', description: 'Starting beat position relative to beat 0.0 of the generated item' },
duration_beats: { type: 'number', minimum: 0.1 },
velocity: { type: 'number', minimum: 0.0, maximum: 1.0 }
},
required: ['pitch', 'start_beat', 'duration_beats', 'velocity']
}
}
},
required: ['mode', 'composition_title', 'target_start_bar', 'target_duration_bars', 'generated_notes']
}
}
};
const REARRANGE_SCENARIOS = [
{
id: 'arpeggio',
@@ -456,6 +507,7 @@ Ví dụ: "Hãy chọn và copy từ bar 4 đến bar 12 của track 1 sau đó
return {
DEFAULT_TOOLS,
REARRANGE_TOOL_SPEC,
REARRANGE_EXTEND_TOOL_SPEC,
REARRANGE_SCENARIOS,
detectRearrangeScenario,
buildRearrangeMessage,
+20 -2
View File
@@ -60,11 +60,13 @@
}
let autoSaveTimer = null;
let lastGetProjectStateCallback = null;
function scheduleTempAutoSave(getProjectStateCallback) {
if (getProjectStateCallback) lastGetProjectStateCallback = getProjectStateCallback;
if (autoSaveTimer) clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(async () => {
try {
const state = getProjectStateCallback();
const state = lastGetProjectStateCallback ? lastGetProjectStateCallback() : null;
if (!state || (!state.tracks && !state.main_session)) return;
const dataJson = JSON.stringify(state);
localStorage.setItem('sonic_temp_project', dataJson);
@@ -76,10 +78,26 @@
}
}, 2000);
}
// Lưu NGAY (bỏ debounce 2s) — dùng cho thay đổi cần bền vững tức thì (đổi màu track)
async function flushTempAutoSave() {
if (autoSaveTimer) { clearTimeout(autoSaveTimer); autoSaveTimer = null; }
try {
const state = lastGetProjectStateCallback ? lastGetProjectStateCallback() : null;
if (!state || (!state.tracks && !state.main_session)) return;
const dataJson = JSON.stringify(state);
localStorage.setItem('sonic_temp_project', dataJson);
if (window.SonicAPI && localStorage.getItem('sonic_token')) {
await window.SonicAPI.saveTempProject(dataJson).catch(() => {});
}
} catch (e) {
console.warn("Flush temp project warning:", e);
}
}
window.SonicStorage = {
exportProjectToSFS,
importProjectFromSFSFile,
scheduleTempAutoSave
scheduleTempAutoSave,
flushTempAutoSave
};
})();
+3 -3
View File
@@ -14,17 +14,17 @@
<script src="/static/js/services/fluidsynthLoader.js?v=202607271245"></script>
<script src="/static/js/services/api.js?v=202607271016"></script>
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
<script src="/static/js/services/storage.js?v=202607271016"></script>
<script src="/static/js/services/storage.js?v=202608038200"></script>
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608031400"></script>
<script src="/static/js/services/aiGateway.js?v=202607271016"></script>
<script src="/static/js/services/aiGateway.js?v=202608037200"></script>
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>
<script src="/static/js/services/ghostNoteExtractor.js?v=202607271727"></script>
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608035800" defer></script>
<script src="/static/js/app.precompiled.js?v=202608038600" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+143 -1
View File
@@ -1,4 +1,146 @@
### [2026-08-03] Task: Remove FX Rack / MIDI Events / Selection panels khỏi bottom dock
### [2026-08-03] Task: Save project MẤT màu track — serializeTracksList thiếu color + schema
- **Tóm tắt thay đổi:** Save project (Cloud/.sfs) → reload → mất màu track. Nguyên nhân: **`serializeTracksList` (8733-8757) KHÔNG serialize `color`** (serializeSafe có color nhưng chỉ là helper temp autosave không dùng; serializeProjectToSchema dùng serializeTracksList) → data lưu server/.sfs không có màu → deserialize (8825 có `color: t.color`) nhận null → mất. Fix:
1. `serializeTracksList`: thêm **`color: t.color || null`** vào track-level fields.
2. `app/models/project_schema.json`: thêm **`color: { type: ["string","null"], default: null }`** vào Track properties (schema validate cho phép + khớp).
(deserialize đã có `color: t.color || default` ✓; validate_project_data không strip color ✓)
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/models/project_schema.json`, `app/templates/index.html` (bump v=202608038600)
- **Ghi chú/Test (nếu có):** BUILD OK 1006266 bytes, node --check OK, `pytest` 86 passed. serialize color ✓, schema color ✓.
---
- **Tóm tắt thay đổi:** Click TCP header (số track "01") → container onClick cũ: nếu track có PIANO ROLL tab với notes → `stopAllPlayback()` + `startSubTabPlayback`**DỪNG main play** (nghe như "mất âm") — đây cũng là cơ chế lỗi "đổi màu → mất âm" trước (click bubble tới container). Fix:
1. Container onClick **chỉ `setSelectedTrackId`** — bỏ toàn bộ auto-play piano roll tab (không phá main playback).
2. Color input thêm `onClick`/`onMouseDown` **stopPropagation** (click chấm màu không bubble → không trigger container select).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608038500)
- **Ghi chú/Test (nếu có):** BUILD OK 1006246 bytes, node --check OK, `pytest` 86 passed. Số track còn ✓, vTrack nguyên vẹn ✓, hết auto-play ✓.
---
- **Tóm tắt thay đổi:** Bản 38300 watchdog rebuild cứu câm — NHƯNG trigger SAI khi nhạc đang ở đoạn im lặng tự nhiên (intro/rest >750ms): rebuild → loop vô hạn → âm không qua main out + master VU đứng im (đúng triệu chứng user báo). Fix:
1. **`anyPlaying` guard**: chỉ đếm im lặng khi có **source ĐANG TRONG KHOẢNG PHÁT** (`ctxNow ∈ [source.startTime, startTime+duration]` — so với audioCtx.currentTime) — đoạn lặng tự nhiên (mọi source ngoài khoảng) → KHÔNG rebuild.
2. **Cooldown 3s** (`lastMasterRebuildTimeRef`) — sau rebuild, 3s mới được rebuild tiếp (phòng lặp).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608038400)
- **Ghi chú/Test (nếu có):** BUILD OK 1006318 bytes, node --check OK, `pytest` 86 passed. anyPlaying ✓, cooldown 3s ✓.
---
- **Tóm tắt thay đổi:** User: nhấn TCP đổi màu → warning `BiquadFilterNode: state is bad` + CÂM ngay sau đó. Warning này từ fast automation (EQ PRO 0.005 — đã sửa setValueAtTime) nhưng **node bị cache dính** (activeTrackNodesRef) → graph hỏng vĩnh viễn → câm. Fix: **master-silence watchdog trong updatePlayhead**: khi đang play + có sources hoạt động nhưng masterBus.analyser im lặng liên tục **>45 frame (~750ms)** → **TỰ ĐỘNG rebuild**: disconnect + xóa toàn bộ track nodes → `initMasterBus()` (graph mới sạch) → `startTrackPlayback(playhead)` → hết câm tự phục hồi (log `[Recovery]`). Reset counter khi có tín hiệu/không play. (Lưu ý: bundle cũ vẫn gây warning — cần hard refresh để EQ PRO setValueAtTime có hiệu lực.)
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608038300)
- **Ghi chú/Test (nếu có):** BUILD OK 1005657 bytes, node --check OK, `pytest` 86 passed. Watchdog ✓, pendingRescheduleRef nguyên vẹn ✓.
---
- **Tóm tắt thay đổi:** 3 fix (sau khi đổi track color):
1. **VU dính animation dù stop + Master VU full**: VU render loop (rAF) chạy mỗi frame bất kể play/stop + không try/catch (exception → loop chết → VU dính giá trị cuối, master full). Fix: **guard `!isPlaying && !recording` → vẽ VU RỖNG (master 0, track -60)** + bọc **try/catch** quanh tick (exception → log, loop vẫn sống).
2. **Màu track không lưu khi reload**: autosave temp debounce 2s — reload nhanh sau đổi màu → mất. Fix: storage.js thêm **`flushTempAutoSave()`** (bỏ debounce — lưu localStorage + saveTempProject NGAY) + `updateTrackColor` gọi schedule + flush với state tracks mới.
3. **Câm sau đổi màu** (chưa tái hiện được): guard audioSig (bản trước) + nếu còn — console `[Play]` logs + `VU tick error` sẽ lộ nguyên nhân.
- **Các file ảnh hưởng:** `app/static/js/services/storage.js` (flushTempAutoSave), `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608038200 cả storage.js)
- **Ghi chú/Test (nếu có):** BUILD OK 1004289 bytes, node --check OK, `pytest` 86 passed.
---
- **Tóm tắt thay đổi:** Đổi màu track → `setTracks` → effect sync (mute/solo/route) chạy → `updateSfRouting()` chạy thừa → SF destination có thể bị đặt sai thời điểm (node chưa tồn tại → setOutputDestination(null)) → play sau đó mất âm thanh. Fix: thêm **`trackAudioSyncSigRef`** — signature audio-relevant (muted/solo/volumeDb/audioBypass/midiBypass + số lượng midiItems/clips/sections) — đổi MÀU/rename (không liên quan audio) → signature giống → **skip toàn bộ re-sync/re-route**; mọi thay đổi audio thực sự (mute/solo/volume/bypass/items) vẫn sync bình thường.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608038100)
- **Ghi chú/Test (nếu có):** BUILD OK 1003238 bytes, node --check OK, `pytest` 86 passed.
---
- **Tóm tắt thay đổi:** TrackStripConsole trước set `height = track.height` ngay trong component — nhưng component CHỈ được dùng ở Mixer panel (F7) → mixer strip bị thu nhỏ bằng track.height (140) trong row cao hơn. Fix: bỏ height cố định — `style: style || undefined` → mixer strip **stretch full row** (items-stretch của container); TCP header panel trái không dùng component này (riêng, đã neo autoHeight).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608038000)
- **Ghi chú/Test (nếu có):** BUILD OK 1002528 bytes, node --check OK, `pytest` 86 passed.
---
- **Tóm tắt thay đổi:** 3 yêu cầu:
1. **Color picker "A user gesture is required"**: input type=color cũ có `pointer-events-none` + label onClick gọi `el.click()` (JS-click bị Chrome chặn). Fix: bỏ el.click — **input phủ label** (`absolute inset-0 w-full h-full opacity-0 cursor-pointer`) → click TRỰC TIẾP vào input (user gesture hợp lệ) — áp cả track header TCP + Sub-Tab editor (vTrack).
2. **Items đổi màu theo track.color**: MIDI items trước dùng màu tím cố định #a78bfa — giờ `(track.color || '#a78bfa')` cho fill/stroke/text/notes; sections fallback `sec.color || track.color` (sec.color riêng vẫn ưu tiên); clips đã theo track.color sẵn.
3. **Mixer strip chỉ 1/2 row** — do fix trước set height track.height TRONG TrackStripConsole (áp cả mixer). KHẮC PHỤC: (đã kiểm tra — height vẫn còn trong component — cần xem lại nếu user còn báo; bản này giữ nguyên vì TCP header dùng chung công thức).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037900)
- **Ghi chú/Test (nếu có):** BUILD OK 1002593 bytes, node --check OK, `pytest` 86 passed. el.click() = 0 ✓, vTrack nguyên vẹn ✓, items/sections theo track.color ✓.
---
- **Tóm tắt thay đổi:** VU meter chỉ nhảy khi play MIDI item — không nhảy khi ARM + nhấn phím MIDI. Nguyên nhân: VU render loop (rAF) tính `midiPeak = isPlaying && isAudible ? midiVuActivityRef[...] : 0``triggerMidiVuActivity` ĐÃ được gọi khi phím MIDI (13658) nhưng bị guard `isPlaying` chặn. Fix: bỏ `isPlaying``midiPeak = isAudible ? midiVuActivityRef[...] : 0` (velocity đã normalize 0-1 trong triggerMidiVuActivity; decay 0.9/frame giữ nguyên) → VU nhảy khi ARM + phím MIDI lẫn khi play item.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037800)
- **Ghi chú/Test (nếu có):** BUILD OK 1002683 bytes, node --check OK, `pytest` 86 passed.
---
- **Tóm tắt thay đổi:** 3 yêu cầu TCP:
1. **Chiều cao TCP = chiều cao timeline row (neo)**: TrackStripConsole nhận `style` prop + tự set `height = track.height || (isArmed ? 164 : 140)` (CÙNG công thức timeline row) — resize track → TCP đổi theo, không lệch; bỏ `overflow-hidden``overflow-y-auto` + center `min-h-[170px]`**các nút (M/S/A/♪/FX/PWR...) không bị che** (đủ chỗ / cuộn được).
2. **Xóa label MIDI note** (khi ARM + nhấn phím MIDI hiện `pitch:velocity:length` sát ô input dropdown).
3. **VU meter lên bên phải, CÙNG HÀNG với input dropdown** (`In: [select] [VU ▓▓▓]` — hiện mọi lúc, không chỉ khi ARM).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037700)
- **Ghi chú/Test (nếu có):** BUILD OK 1002496 bytes, node --check OK, `pytest` 86 passed. Label note đã xóa ✓, VU cùng hàng ✓, TCP neo height ✓.
---
- **Tóm tắt thay đổi:** User báo play + ARM MIDI preview đều không có âm thanh; console: `BiquadFilterNode: state is bad, probably due to unstable filter caused by fast parameter automation` — đúng cảnh báo cũ trong code (fast automation → master routing broken → CÂM toàn cục). Thủ phạm: EQ PRO `setBand`/`setAmount` dùng `setTargetAtTime(..., now, 0.005)` — automation 5ms quá nhanh → Chromium đánh dấu filter unstable vĩnh viễn (node cache dính). **Fix: đổi toàn bộ sang `setValueAtTime(x, now)`** (tức thì, không automation ramp → không flag) — 4 chỗ (freq/Q/gain trong setBand + gain trong setAmount). Hard refresh → module EQ PRO mới (filter mới) → hết câm.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037600)
- **Ghi chú/Test (nếu có):** BUILD OK 1002801 bytes, node --check OK, `pytest` 86 passed. Không còn setTargetAtTime 0.005 (EQ PRO) — setValueAtTime ✓.
---
- **Tóm tắt thay đổi:** Track `[AI Var]` do AI tạo không có âm thanh — thiếu cấu trúc như track MIDI gốc (instrumentProgram/instrumentName/synth_engine + các field rules). Fix: track mới = **clone toàn bộ track nguồn** (`...(srcTrack)`) — kế thừa synth_engine, instrumentProgram/Name, volumeDb, pan, fxActive, bypass flags, color... — rồi reset phần content (id, name `[AI Var] title`, buffer null, clips/sections rỗng, midiItems = [item AI], muted/solo false, markers rỗng, serverFileId null); fallback synth_engine/instrument từ aiResult hoặc item gốc nếu track nguồn thiếu. Track AI giờ tuân thủ rules như mọi track khác trong session.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037500)
- **Ghi chú/Test (nếu có):** BUILD OK 1002829 bytes, node --check OK, `pytest` 86 passed. Clone srcTrack ✓, instrumentProgram kế thừa ✓.
---
- **Tóm tắt thay đổi:** Debug log cho thấy provider trả **nhiều tool calls `_unknown`** (arguments chỉ chứa "reason" — không có notes) → tool-calling không hoạt động với provider hiện tại. Fix: **KHÔNG gửi `tools`** (`tools: []`); prompt yêu cầu **JSON thuần** (không markdown) với shape chính xác `{mode, composition_title, target_start_bar, target_duration_bars, generated_notes[]}` + constraint "notes phủ 0→total_beats, kết thúc sạch, start_beat relative". Parse: **ưu tiên textResponse** (JSON); functionCalls chỉ dùng khi `name === 'rearrange_or_extend_midi_melody'` (bỏ qua _unknown). Giữ `pickNotes` (generated_notes/notes/rearranged_notes/midi_notes/mảng thuần) + diagnostic raw khi vẫn fail.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037400)
- **Ghi chú/Test (nếu có):** BUILD OK 1002469 bytes, node --check OK, `pytest` 86 passed. tools:[] ✓, pickNotes ×2, JSON shape constraint ✓.
---
- **Tóm tắt thay đổi:** Lỗi "AI không trả về generated_notes hợp lệ" — model trả key/format khác (notes/rearranged_notes/mảng thuần) hoặc functionCalls arguments là mảng trực tiếp. Fix: `pickNotes(obj)` nhận `generated_notes | notes | rearranged_notes | midi_notes` hoặc mảng thuần; functionCalls arguments là mảng → bọc lại; textResponse parse → pickNotes; Khi vẫn fail: log `console.error` chi tiết (raw text 600 chars + functionCalls 500 + textResponse 500) + action log kèm Raw 200 chars để user dán lại chẩn đoán.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037300)
- **Ghi chú/Test (nếu có):** BUILD OK 1001539 bytes, node --check OK, `pytest` 86 passed. pickNotes ×5, aiNotes ×2.
---
- **Tóm tắt thay đổi:** Sửa tool AI theo spec đính kèm — chỉ cần click MIDI item + gõ prompt:
1. **STEP 1 — Extract/Nén**: `SonicMidiExtractor.extractSelectedMIDIContext(tracks, itemId, bpm)` — note gồm `note_name` (C4/D4 — tiết kiệm token), start_beat/duration_beats/velocity round 2dp; context: track/item, duration_bars, total_beats, bpm, total_notes.
2. **STEP 2 — Function Tool Schema**: thêm `REARRANGE_EXTEND_TOOL_SPEC` vào aiGateway.js (name `rearrange_or_extend_midi_melody`, 2 mode SIMILAR_VARIATION/EXTEND_CONTINUATION, params: mode, composition_title, target_start_bar, target_duration_bars, soundfont_id/bank/program, generated_notes[]) + export + window.AIGateway.
3. **STEP 3 — Prompt Engineering**: `handleAiComposeFromItem(mode)` build prompt đúng spec: ORIGINAL CONTEXT (track/item/bpm/time_sig/location/notes JSON) + USER DIRECTIVE + MODE DIRECTIVE (Extend: continuation từ bar kế; Variation: equal length, giữ hòa âm + kỹ thuật biến tấu) + STRICT CONSTRAINTS (bắt buộc function tool, notes phủ 0→total_beats, kết thúc sạch).
4. **STEPS 4-5 — Dispatch + Ingest**: gửi kèm `tools: [REARRANGE_EXTEND_TOOL_SPEC]`; decode functionCalls (fallback text JSON); **SIMILAR_VARIATION → track mới `[AI Var] title` ngay dưới track nguồn (A/B) + soundfont từ item gốc; EXTEND_CONTINUATION → append item `[Extend] title` cuối track gốc** (target_start_bar × secPerBar → giây).
5. **UI**: nút mode **Variation/Extend** (toggle, amber/sky) + nút **Compose** gọi `handleAiComposeFromItem(aiComposeMode)`; state `aiComposeMode`.
- **Các file ảnh hưởng:** `app/static/js/services/aiGateway.js` (REARRANGE_EXTEND_TOOL_SPEC), `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037200 cả aiGateway.js)
- **Ghi chú/Test (nếu có):** BUILD OK 1000935 bytes, node --check OK, `pytest` 86 passed. handleAiComposeFromItem ×3, REARRANGE_EXTEND ×1, rearrange_or_extend ×2.
---
- **Tóm tắt thay đổi:** Tool AI mới theo yêu cầu: click MIDI item trên timeline → AI panel hiện nút **"Compose"** (teal, icon music-2; mờ khi chưa chọn item) → gõ prompt → **`handleAiComposeToNextTrack`**:
1. Lấy cấu trúc giai điệu item (`getSelectedMidiItemInfo` mở rộng trả notes/startTime/duration): noteCount, pitchRange, pitches, starts, durations, velocities (≤60 notes).
2. Gửi AIGateway: "Compose NEW melody SIMILAR in style/rhythm/motif but NOT identical" + yêu cầu user → parse JSON notes (giống handleAISend).
3. **Track kế tiếp**: track sau track nguồn — nếu RỖNG (không buffer/clips/midiItems/sections) → ghi vào đó; nếu KHÔNG RỖNG → **chèn track mới "AI Melody Track"** ngay sau track nguồn.
4. Tạo MIDI item (id midi_ai_*, startTime 0, duration = maxBeat × beatSec, name "AI Melody (tên item)") + notes mới → push vào track mục tiêu; action log + toast.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037100)
- **Ghi chú/Test (nếu có):** BUILD OK 996444 bytes, node --check OK, `pytest` 86 passed. handleAiComposeToNextTrack ×2 (định nghĩa + onClick).
---
- **Tóm tắt thay đổi:**
1. **Re-schedule vẫn không chạy khi kéo clip**: rAF loop giữ `updatePlayhead` của RENDER CŨ — closure nắm `activeTracks`/`sessionTabs` STALE (effect deps không gồm chúng) → signature luôn cũ → không bao giờ phát hiện kéo. **Fix**: re-schedule dùng `activeTracksRef.current` + `sessionTabsRef.current` (sync mỗi render); solo check tính lại từ ref (`curTracks.some(t => t.solo)`).
2. **Zoom persist**: `zoom` (App) khởi tạo từ `localStorage.sf_zoom` + effect lưu khi đổi → kích thước items giữ nguyên sau reload (zoom in/out).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037000)
- **Ghi chú/Test (nếu có):** BUILD OK 989793 bytes, node --check OK, `pytest` 86 passed.
---
- **Tóm tắt thay đổi:** Debounce 250ms cũ chờ sig "ổn định" — trong lúc kéo clip liên tục, sig đổi mỗi frame → KHÔNG BAO GIỜ re-schedule → clip kéo đi vẫn phát âm thanh cũ (không cập nhật realtime). Thay bằng **THROTTLE ~300ms**: khi sig đổi (dù đang kéo hay không) → re-schedule định kỳ (≤3.3 lần/giây, không giật) — clip kéo đi dừng ngay ≤300ms; clip mới phát khi playhead tới. `triggerRescheduleNow` (mouseup) reset cooldown → re-schedule ở check kế (~100ms) sau khi thả. Giữ mode-aware (solo/loop local/toàn session) + signature đầy đủ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608036900)
- **Ghi chú/Test (nếu có):** BUILD OK 989194 bytes, node --check OK, `pytest` 86 passed.
---
- **Tóm tắt thay đổi:** `triggerRescheduleNow()` — set `pendingRescheduleRef = { sig hiện tại, time: 0 }` → check kế tiếp trong updatePlayhead (~100ms) re-schedule LUÔN (time 0 → điều kiện >250ms thoả) — gọi ở mouseup của drag clip (`draggedClipRef`) + drag section/midi item (`draggedSectionItemRef`). Kịch bản: playhead bar 3, clip ở bar 3 → kéo clip tới bar 5 (playhead 3 không còn âm thanh ✓ — clip schedule ở 5) → kéo clip QUAY LẠI bar 3 → **thả → ~100ms → re-schedule → phát NGAY** (playOffset = playhead clipStart ≈ 0 → phát từ đầu clip). Debounce 250ms chỉ còn dành cho thao tác kéo liên tục (chống giật).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608036800)
- **Ghi chú/Test (nếu có):** BUILD OK 989510 bytes, node --check OK, `pytest` 86 passed. triggerRescheduleNow ×3.
---
- **Tóm tắt thay đổi:** `buildItemsSignature` thêm **track-level default clip** (`track.buffer``def:startTime:speed`) — vị trí clip default lưu ở `track.startTime`, KHÔNG nằm trong `t.clips` → trước đây kéo default clip không đổi signature → không re-schedule (vẫn phát nội dung cũ). Kết hợp với re-schedule mode-aware (loop local/solo chỉ phát track liên quan) + debounce 250ms: kịch bản "playhead bar 3 trong clip → kéo clip tới bar 3" → sau khi thả, re-schedule `pt = playhead``playOffset = pt clip.startTime = 0`**phát TỪ ĐẦU clip realtime**; clip nằm trước playhead → phát từ offset tương ứng.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608036700)
- **Ghi chú/Test (nếu có):** BUILD OK 988860 bytes, node --check OK, `pytest` 86 passed.
---
- **Tóm tắt thay đổi:** Re-schedule (khi items đổi vị trí lúc play) trước đây luôn gọi `startTrackPlayback(pt)` → LOOP LOCAL bị phá (phát nhầm các track khác + mất hành vi loop). Fix: re-schedule theo ĐÚNG chế độ play — `hasAnySolo` → chỉ track solo (`startLocalTrackPlayback`); `selectionMode==='local'` → chỉ `localSelectionTrackId`; ngược lại `startTrackPlayback(pt)`. Cả 2 hàm tính `playOffset = pt clip.startTime`**kéo clip tới đúng vị trí playhead → phát TỪ ĐẦU clip** (offset 0) realtime (debounce 250ms của user giữ nguyên — tránh stop/start liên tục khi kéo).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608036600)
- **Ghi chú/Test (nếu có):** BUILD OK 988538 bytes, node --check OK, `pytest` 86 passed.
---
- **Tóm tắt thay đổi:** Khi đang play/loop mà user kéo/thay đổi vị trí item → vẫn phát nội dung cũ (source đã schedule với startTime cũ). Fix: **`buildItemsSignature(tracksList, tabsList)`** (module-level — signature vị trí/speed/duration của clips + midiItems + sections + nội dung section tab); `scheduledItemsSigRef` được capture mỗi lần `startTrackPlayback`; `updatePlayhead` kiểm tra ~10fps (mỗi 6 frame, guard `activeTab==='main'` + không RECORDING): nếu signature ĐỔI → `stopAllPlayback()` + `startTrackPlayback(playhead hiện tại)` — item mới phát đúng vị trí mới gần như realtime.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608036500)
- **Ghi chú/Test (nếu có):** BUILD OK 987414 bytes, node --check OK, `pytest` 86 passed. buildItemsSignature ×3, scheduledItemsSigRef ×4, playheadFrameCountRef ×3.
---
- **Tóm tắt thay đổi:** Click tempo track lane (không tạo selection) → nhấn nút Loop → auto-derive `0 → (maxEnd + 2 bars)` (bar 18 trong khi maxDuration chỉ tới bar 16). Fix: dùng **`computeMainSessionEndTime(activeTracks)`** làm maxEnd (clips theo buffer.duration/speed, midiItems endTime/duration, section bounds — đầy đủ hơn logic cũ vốn bỏ sót speed + midiItems + sections) và **BỎ `+ secPerBar * 2`** — loop region = đúng endtime của session.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608036400)
- **Ghi chú/Test (nếu có):** BUILD OK 985447 bytes, node --check OK, `pytest` 86 passed. Không còn `maxEnd + secPerBar * 2` trong bundle.
---
- **Tóm tắt thay đổi:** `maxDuration` = endtime + **12 bars buffer + scrollBufferExtra** (dành cho scroll/zoom) — nhưng updatePlayhead dùng nó làm điểm dừng LOOP → loop kéo dài quá duration thật. Thêm **`projectEnd`** (useMemo + `projectEndRef`): endtime THẬT của session (main → `computeMainSessionEndTime(activeTracks)`; section-tab → `secStart + computeMainSessionEndTime(tab.tracks)`; RECORDING → +60s; tối thiểu 1s) — KHÔNG buffer. `updatePlayhead` (nhánh hết bài) đổi `maxDurationRef.current`**`projectEndRef.current`** → master loop play lại từ 0 đúng tại endtime; maxDuration giữ nguyên cho scroll/zoom/minZoom.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608036300)
- **Ghi chú/Test (nếu có):** BUILD OK 985502 bytes, node --check OK, `pytest` 86 passed. projectEndRef ×3, loop stop dùng projectEnd ✓, secPerBar*12 còn (scroll).
---
- **Tóm tắt thay đổi:** Hành động **alt-click-drag** (speed stretch) ở rìa phải clip trong section-tab graph giờ có undo/redo: `stretchStartRef` lưu thêm `finalSpeed` (cập nhật liên tục khi kéo) + `clipName`; `handleMouseUp` push entry **`SET_CLIP_SPEED`** vào `window.UndoRedoEngine` (chỉ khi speed thực sự đổi > 0.001): undo → `onSpeedChange(before)` (speed gốc), redo → `onSpeedChange(after)``onSpeedChange` tự tính lại volumeNodes/panningNodes/fade/label theo ratio nên khớp cả 2 chiều. Phím Ctrl+Z / nút Undo ưu tiên UndoRedoEngine (đã có sẵn) nên hành động này undo/redo ngay.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608036200)
- **Ghi chú/Test (nếu có):** BUILD OK 984340 bytes, node --check OK, `pytest` 86 passed. SET_CLIP_SPEED ×2, finalSpeed ×8 trong bundle.
---
- **Tóm tắt thay đổi:** `computeProjectEndTime` (gộp section content vào main) thay bằng **`computeMainSessionEndTime(tracksList)`**: chỉ tính endtime của items TRÊN tracks đó (clips + midiItems + section bounds) — **mặc kệ nội dung SECTION-TAB** (khi play trong main session, sub-track items bị clamp trong section bounds nên tab dài không kéo dài project). `maxDuration` useMemo tách theo `activeTab`: MAIN → `computeMainSessionEndTime(activeTracks)`; SECTION-TAB → `secStart(section) + computeMainSessionEndTime(tab.tracks)` (tab.sectionId → tìm section start trên main). Export (bounce + offline) dùng `computeMainSessionEndTime` (main-based, giữ max với midiCache preview).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608036100)
- **Ghi chú/Test (nếu có):** BUILD OK 983477 bytes, node --check OK, `pytest` 86 passed. computeMainSessionEndTime ×5, computeProjectEndTime = 0.
---
- **Tóm tắt thay đổi:** `maxDuration` (loop stop) + `triggerBounceExport` + `clientSideExport` tính end time THIẾU nội dung SECTION-TAB (sub-track clips/MIDI bên trong section + session tabs độc lập) → loop dừng sớm / export cắt cuối bài. Thêm **`computeProjectEndTime(tracksList, tabsList)`** (module-level): gộp clips (start + duration/speed), midiItems (endTime ưu tiên, fallback startTime + duration), sections (start + duration) VÀ nội dung bên trong từng section (sub-track clips schedule tại sec.start + local, sub MIDI tại sec.start + item.startTime) + session tabs độc lập. Áp dụng đồng bộ 3 nơi: `maxDuration` useMemo (thêm sessionTabs vào deps), `triggerBounceExport` durationLimit, `clientSideExport` durationLimit (giữ max với midiCache).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608036000)
- **Ghi chú/Test (nếu có):** BUILD OK 983950 bytes, node --check OK, `pytest` 86 passed. computeProjectEndTime ×4 trong bundle.
---
- **Tóm tắt thay đổi:**
1. **Export modal float không tương tác được** → chuyển từ JSX inline trong IIFE dockPanels thành **component riêng `ExportModal`** (render ở App level cạnh FXRackModal — cùng vị trí với modal đã chứng minh hoạt động tốt). Nội dung giữ nguyên: Nguồn/Định dạng/SR/Bit/Chất lượng/Kênh + nút Bounce MIDI + Export. Loại bỏ mọi nghi vấn stacking-context/pointer-events từ IIFE.
2. **Thêm diagnostic log `[Play]`** trong startTrackPlayback (offset, số track, masterBus, destination, node ok từng track) — để xác định lỗi "không có âm thanh ra main out khi play": user dán console log (F12) lại để tôi chẩn đoán chính xác.
3. Kiểm tra: `getOrCreateSubTrackNode` + section playback + getOrCreateTrackNode (fxEntry/SF chain/route) + initMasterBus (input→compressor→inputAnalyser→outputAnalyser→output→destination) + dryInput→dryOutput→output + computeTrackAudibleGain — TẤT CẢ ĐÚNG, không có lỗi tĩnh.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608035900)
- **Ghi chú/Test (nếu có):** BUILD OK 982816 bytes, node --check OK, `pytest` 86 passed. ExportModal ×2 (định nghĩa + render), [Play] log có trong bundle.
---
- **Tóm tắt thay đổi:** Theo yêu cầu: xóa 3 panel khỏi dock rows (bottom) — `addPanel('selection'/'fx_rack'/'midi_events', ...)` bị bỏ (không render bottom row nữa); 3 nút toolbar tương ứng (Selection / FX Rack / MIDI Events) bị xóa (regex chính xác, verify syntax từng bước). FX Rack vẫn dùng modal float (nút FX trên track strip → `__openFxRack`); Export vẫn là modal float (tooltip "Export (floating modal)"); Mixer (F7) + Media Explorer (F6) + AI/Python Tools giữ nguyên.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608035800)
- **Ghi chú/Test (nếu có):** BUILD OK 977199 bytes, node --check OK, `pytest` 86 passed. Verify: addPanel + onClick của 3 panel = False trong source; Export/Mixer/Media Explorer còn.