43 Commits

Author SHA1 Message Date
3dtours fa24c61bbf FIX: hiddenimport tinh scipy.stats._ansari_swilk_statistics - phong collect_submodules miss (scipy 1.18+ _morestats import top-level) 2026-08-08 15:07:37 +00:00
3dtours 83427fe503 FIX: verify_bundle.py false-positive Windows - TOC Python literal double-escape backslash (app\\static -> app//static), normalize 2 buoc 2026-08-08 14:50:07 +00:00
3dtours cef2d6666b FIX (lan 3 - goc root): chen _SPEC_ROOT vao sys.path truoc collect_data_files('app') — 'pyinstaller' entry point khong them CWD vao sys.path 2026-08-08 14:37:38 +00:00
3dtours bfe9e47db1 FIX (lan 2): build_windows.ps1 tu cai @babel/standalone neu thieu (ERR_MODULE_NOT_FOUND) 2026-08-08 14:19:03 +00:00
3dtours bcc91db4a7 FIX: them @babel/standalone vao package.json (build.mjs ERR_MODULE_NOT_FOUND tren Windows) 2026-08-08 14:17:15 +00:00
3dtours 1f7a8c6f1b FIX: verify bundle dung tools/verify_bundle.py (normalize backslash, false-positive 'thieu static') + check engine.spec phien ban moi 2026-08-08 14:15:14 +00:00
3dtours 8b0551b18f FIX: build_windows.ps1 parse error PS5.1 — ASCII-only + UTF-8 BOM (unicode em-dash lam hong string terminator) 2026-08-08 14:01:40 +00:00
3dtours eaca051191 FIX (lan 2): bundle app assets qua collect_data_files (import-system) + fallback static dir trong main.py + verify TOC trong build_windows.ps1 2026-08-08 13:59:35 +00:00
3dtours b78a629429 IMPROVE: About modal dùng logo favicon.svg của app thay cho div chữ SF 2026-08-08 13:37:03 +00:00
3dtours 366219a269 FEAT: menu Help (About modal + hướng dẫn sử dụng) + Tools Preferences (theme/language/button font size) 2026-08-08 13:23:23 +00:00
3dtours 3618e3c591 FIX: VU meter giữ animation đúng trường độ âm khi ARM + giữ phím MIDI keyboard (heldMidiNotesRef, không decay khi note còn giữ) 2026-08-08 13:12:49 +00:00
3dtours b490aa9951 FIX: chặn browser zoom toàn trang — item/button/label giữ nguyên kích thước khi phóng to UI (capture preventDefault, giữ zoom chuyên dụng) 2026-08-08 13:08:25 +00:00
3dtours be93f9f55a FIX: bundle scipy submodules (ansari_swilk_stats) + librosa .pyi stubs (lazy_loader) trong engine.spec 2026-08-08 12:52:38 +00:00
3dtours 7dda81b1d3 FIX: Windows build crash 'app\static does not exist' (datas absolute + APP_DIR freeze-aware) + VU meter ARM/MIDI keyboard 2026-08-08 11:51:15 +00:00
3dtours f7b0172fdc FIX: engine.spec — collect_submodules('celery.fixups') thay vi liet ke tung module 2026-08-08 10:40:21 +00:00
3dtours 9bebbe8e7a FIX: engine.spec — hiddenimport celery.fixups (celery import dong, PyInstaller khong thay) 2026-08-08 10:24:35 +00:00
3dtours 4e4391b315 FIX: lib.rs — E0597 borrow, take child ra khoi lock truoc khi kill 2026-08-08 10:12:21 +00:00
3dtours e978abf0e9 FIX: lib.rs — import tauri::Manager (can manage/state) 2026-08-08 10:04:58 +00:00
3dtours bb699631bf FIX: tauri.conf.json v2 — nsis config phai nam trong bundle.windows 2026-08-08 09:55:27 +00:00
3dtours 0a1f3efd64 FEAT: chuẩn bị thư viện để tạo file exe trên window 2026-08-08 09:44:37 +00:00
3dtours 9ef622f29a IMPROVE: thêm tập tin LICENSE của dự án 2026-08-08 07:20:09 +07:00
3dtours 8e3c652551 IMPROVE: PIANO ROLL TAB khi play thì playhead luôn nằm chính giữa piano roll 2026-08-07 22:10:37 +07:00
3dtours 393df8fd7b FIX: cho phép shift+click để chọn notes, drag để di chuyển các notes, ctrl+drag để copy nhanh các notes 2026-08-07 21:48:14 +07:00
3dtours 688a7a57e2 FIX: cho phép copy/cut các notes được chọn trong PIANO ROLL TAB 2026-08-07 21:39:18 +07:00
3dtours 652e745d0a IMPROVE: thay đổi giao diện của PIANO ROLL TAB 2026-08-07 21:22:13 +07:00
3dtours 45ab31d42e IMPROVE: thêm các tính năng cho MIDI trong PIANO ROLL TAB 2026-08-07 20:46:47 +07:00
3dtours c3182f6baa FIX: sửa lỗi không hiển thị đúng nội dung của SECTION-TAB trong section item 2026-08-07 18:58:29 +07:00
3dtours 1d9b426d15 FIX: sửa lỗi copy/cut paste không đúng item 2026-08-07 17:06:05 +07:00
3dtours 4f34491f3e FIX: sửa lỗi hiển thị animation của VU meter khi play MIDI 2026-08-07 15:57:49 +07:00
3dtours 9e269d3b79 FIX: sửa lỗi hiển thị animation của VU meter 2026-08-07 15:40:15 +07:00
3dtours 58c6ec794d FIX: sửa lỗi hiển thị số bar bị sai 2026-08-07 15:07:06 +07:00
3dtours 173a1f227b FIX: sửa lỗi lưu thay đổi trong SECTION-TAB thì cũng tạo duration trên MAIN SESSION 2026-08-07 11:08:39 +07:00
3dtours c1aa1eb4d4 FIX: sửa lỗi phát hiện đã không còn play midi note để PIANO ROLL TAB quay về vị trí 0 2026-08-07 10:41:45 +07:00
3dtours 6c16bf8410 FIX: sửa lỗi đồng bộ playhead vị trí play giữa MAIN SESSION/SECTION-TAB/PIANO ROLL TAB 2026-08-07 10:37:34 +07:00
3dtours 4981785913 FIX: sửa lỗi playhead đồng bộ giữa các tab, nhấn space có thể bind với phím play 2026-08-07 10:27:21 +07:00
3dtours bca661bffd FIX: sửa lỗi chuyển từ tab PIANO ROLL TAB sang MAIN SESSION/SECTION-TAB không đồng bộ playhead 2026-08-07 10:12:18 +07:00
3dtours 4c820bfa77 FIX: sửa lỗi chuyển từ tab PIANO ROLL TAB sang MAIN SESSION và SECTION-TAB thì bị câm 2026-08-07 09:35:40 +07:00
3dtours 8cdf2942b9 FIX: sửa lỗi chuyển từ tab MAIN SESSION sang SECTION-TAB và PIANO ROLL TAB thì bị bypass 2026-08-07 09:13:03 +07:00
3dtours 23675dab83 FIX: Đã fix preview Media Explorer (v202608062345) 2026-08-06 20:45:56 +07:00
3dtours 71b9231b57 FIX: Đã fix block lồng + mất MIDI (v202608062330) 2026-08-06 20:42:27 +07:00
3dtours 93dc13b17a FIX: lỗi bypass của SECTION-TAB 2026-08-06 18:19:04 +07:00
3dtours f1e914fa62 FIX: lỗi: CC7 (channel volume) chỉ được set 100/0 (mute/unmute) — volume track KHÔNG áp qua channel → track MIDI dùng chung channel: CC7 của track set cuối thắng → volume của track kia vô tác dụng 2026-08-06 14:10:57 +07:00
3dtours 4dfab954d8 FIX: reset tất cả về mặc định khi tạo new project 2026-08-06 11:47:10 +07:00
38 changed files with 4913 additions and 466 deletions
+44
View File
@@ -0,0 +1,44 @@
# ── Docker build context — loại bỏ mã nguồn/bí mật không cần vào image ──
.git
.gitignore
.github
# Mã nguồn client (JSX) — image CHỈ chứa bản precompiled (minified)
app/static/js/app.jsx
app/static/js/app.jsx.new
*.jsx
# Bí mật
.env
.env.*
!.env.example
# Dependencies / build cache
node_modules
__pycache__
*.pyc
.cache
.pytest_cache
# Tài liệu / kế hoạch / backup nội bộ
wiki.md
PLAN.md
PLAN*
plans/
md/
*.patch
*.recovered
index.html.recovered
celerybeat-schedule
celerybeat.pid
# Dữ liệu runtime (mount volume riêng — KHÔNG chép vào image)
app/storage/uploads
app/storage/processed
app/storage/sonicforge.db
celerybeat-schedule
# Test
tests/
tools/
samples/
+17
View File
@@ -0,0 +1,17 @@
# ── SonicForge Studio — môi trường (SAO CHÉP sang .env — KHÔNG commit .env) ──
# Redis / Celery
REDIS_URL=redis://redis:6379/0
CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/0
# LLM (server-side proxy — client không thấy key)
OPENAI_API_BASE=http://localhost:11434/v1
OPENAI_API_KEY=ollama
# Auth
SECRET_KEY=thay-bang-chuoi-ngau-nhien-dai
DEFAULT_ADMIN_PASSWORD=thay-mat-khau-admin
# Storage (đường dẫn trong container)
STORAGE_DIR=/app/app/storage
+4 -1
View File
@@ -28,4 +28,7 @@ samples/
.vscode/
*.log
celerybeat-schedule
node_modules
node_modules
src-tauri/target/
src-tauri/binaries/
src-tauri/vc_redist.x64.exe
+87
View File
@@ -0,0 +1,87 @@
# DESKTOP INSTALL PLAN — SonicForge Studio (bản cài trực tiếp trên OS)
Mô hình: **server chạy nền trên máy người dùng (localhost), client mở bằng browser**.
Không cần Electron — tận dụng stack hiện có (FastAPI + static JS precompiled).
Soundfonts do **người dùng tự tải vào thư mục riêng****khai báo** cho ứng dụng.
---
## 1. KIẾN TRÚC
```
┌─────────────────────────── NGƯỜI DÙNG (1 máy) ───────────────────────────┐
│ Browser ──http://127.0.0.1:8000──▶ FastAPI (uvicorn, chạy NỀN) │
│ │ │
│ ~/SonicForgeStudio/ ├─ data/ (DB, uploads, cache) │
│ ├─ data/sonicforge.db ├─ soundfonts/ (user tự bỏ .sf2) │
│ ├─ soundfonts/*.sf2/.sf3 └─ quét nền 30s (scanner có sẵn) │
│ └─ config.json ← khai báo folder soundfont │
└──────────────────────────────────────────────────────────────────────────┘
```
## 2. ĐÓNG GÓI CÀI ĐẶT (per-OS) — server nền + browser client
### Thành phần đóng gói
| Thành phần | Vai trò |
|---|---|
| Python backend (`app/`, `requirements.txt`) | Server — **PyInstaller `--onedir`** → binary (không cần Python trên máy user; bảo vệ source tốt hơn so với chạy .py) |
| `app/static/` (precompiled.js + css + vendor) | Client — đóng vào package, server phục vụ |
| VST3 plugins (`vst_plugins/`) | Copy theo platform (win .dll / mac .vst3 / linux .so) |
| Service wrapper | Tự khởi động server nền + mở browser khi login |
### Cài đặt + service nền theo OS
| OS | Installer | Service nền | Auto-open browser |
|---|---|---|---|
| **Windows** | Inno Setup (`.exe`) | Windows Service qua `NSSM`/`WinSW` hoặc Task Scheduler (logon) | `start http://127.0.0.1:8000` |
| **macOS** | `.dmg` + `.app` (PyInstaller) | `launchd` LaunchAgent (`~/Library/LaunchAgents`) | `open http://127.0.0.1:8000` |
| **Linux** | `.deb` / `.AppImage` | systemd **user** service (`~/.config/systemd/user/`) | `xdg-open http://127.0.0.1:8000` |
- Server bind **`127.0.0.1`** (không lộ mạng), port mặc định 8000 (config được).
- App gồm 2 tiến trình nhỏ: `web` (uvicorn) + `worker` (celery) — hoặc gộp worker vào web ở chế độ desktop (đơn giản: `--pool=solo`, chạy celery trong tiến trình riêng nếu cần render nặng).
## 3. THƯ MỤC DỮ LIỆU NGƯỜI DÙNG
```
~/SonicForgeStudio/
├── data/ # DB, uploads, processed (thay app/storage khi chạy desktop)
├── soundfonts/ # USER tự tải .sf2/.sf3 vào đây (mặc định được quét)
└── config.json # cấu hình: soundfont_dirs, port, autostart...
```
- `config.py`: thêm `DATA_DIR` (env `SFDATA_DIR`, mặc định `~/SonicForgeStudio/data`), `SOUNDFONT_DIRS`.
## 4. SOUNDFONT DO NGƯỜI DÙNG QUẢN LÝ (tải + khai báo)
**Cơ chế hiện có (tận dụng):** `app/core/soundfont_scanner.py``SoundFontAutoScanner` quét nền 30s
`/opt/daw_engine/soundfonts` + `storage/soundfonts` → catalog → API list qua `app/api/v1/plugins.py`.
**Việc cần làm (mở rộng):**
1. **Scanner nhận folder người dùng:** constructor nhận thêm `user_dirs` (từ `SOUNDFONT_DIRS` env + `config.json`) — merge vào catalog (ưu tiên: user > system).
2. **Khai báo folder — 2 cách:**
- **UI** (chính): Settings → "Soundfonts" → nút **Add folder…** (chọn thư mục chứa .sf2) → lưu vào `config.json` → gọi scanner rescan.
- **config.json** (thủ công): `{ "soundfont_dirs": ["D:/SF", "/Users/me/sf"] }`.
3. **API:** thêm endpoint `POST /api/v1/plugins/soundfonts/dirs` (đăng ký folder) + `GET .../dirs` (liệt kê) — scanner reload.
4. **UI danh sách:** hiển thị catalog (tên SF, kích thước, folder nguồn) — chọn = load vào FluidSynth (luồng có sẵn qua `SonicSF.selectInstrument`).
5. **Số hóa:** không upload file — server đọc trực tiếp từ đường dẫn user khai báo (không nhân đôi dữ liệu).
## 5. BUILD + PHÁT HÀNH
```
code → build.mjs (precompiled + ?v=) → PyInstaller (server binary) → đóng installer theo OS
→ ký số (tùy chọn) → phát hành (GitHub Releases / trang riêng)
```
- **GitHub Actions matrix** (windows-latest / macos-latest / ubuntu-latest): test → build → installer artifact.
- Installer gồm: binary server, static/, VST plugins nền tảng, script tạo service + mở browser, mặc định tạo `~/SonicForgeStudio/` lần chạy đầu.
## 6. CẬP NHẬT
- **Version check**: khi mở app, gọi endpoint version (file `version.json` đóng kèm + so sánh remote) → thông báo bản mới + link tải installer.
- Cập nhật = chạy installer mới (ghi đè, GIỮ NGUYÊN `~/SonicForgeStudio/` — data + soundfonts không đụng).
- `?v=` cache-bust JS mỗi bản (cơ chế đã có) — browser không dính cache cũ.
## 7. CHECKLIST CODE CẦN LÀM
- [ ] `config.py`: `DATA_DIR`, `SOUNDFONT_DIRS` (env + config.json)
- [ ] `soundfont_scanner.py`: nhận `user_dirs`, merge catalog, ưu tiên user
- [ ] API: `POST/GET .../soundfonts/dirs` (đăng ký/liệt kê folder) + rescan
- [ ] UI Settings → Soundfonts (Add folder, Browse, list, load)
- [ ] `soundfontStorage.js`: chuyển hướng sang catalog folder-scan (giữ upload path cho dự án cũ)
- [ ] PyInstaller spec: bundle static/ + vendor (libfluidsynth wasm, vst), bind 127.0.0.1
- [ ] Service wrappers: Windows (NSSM/WinSW), macOS (launchd), Linux (systemd user) + auto-open browser
- [ ] Installer scripts: Inno Setup (.exe), dmg (macOS), deb/AppImage (Linux)
- [ ] CI matrix build 3 OS + release artifacts
- [ ] `version.json` + in-app update check
+85
View File
@@ -0,0 +1,85 @@
# DISTRIBUTION PLAN — SonicForge Studio (Alpha → Beta → Production)
Mục tiêu: phân phối ứng dụng cho người dùng **không chia sẻ mã nguồn**, có pipeline
cập nhật liên tục. Áp dụng cho backend Python/FastAPI + frontend JS (Babel precompiled).
---
## 1. TÁCH BÍ MẬT — FILE .env
| Biến | Dùng cho | Bắt buộc |
|---|---|---|
| `REDIS_URL` | Redis/Celery broker | ✓ |
| `CELERY_BROKER_URL` / `CELERY_RESULT_BACKEND` | Celery | ✓ |
| `OPENAI_API_BASE` / `OPENAI_API_KEY` | LLM server-side (client KHÔNG thấy key — `aiGateway.js` chỉ gọi proxy server) | ✓ |
| `SECRET_KEY` | Auth token (app/core/auth.py) | ✓ (mặc định trống → phải set) |
| `DEFAULT_ADMIN_PASSWORD` | Admin mặc định | thay đổi ở production |
- File `.env.example` đã tạo (commit được). `.env` thật KHÔNG commit (đã vào `.dockerignore`).
- **Nguyên tắc:** client JS không bao giờ chứa secret — mọi API key nằm server (env) hoặc proxy.
## 2. BUILD IMAGE — KHÔNG CHIA SẺ MÃ NGUỒN
### 2.1 Chặn source khỏi image (`.dockerignore` — đã tạo)
Loại khỏi build context: `.git`, `app/static/js/app.jsx` (source JSX), `wiki.md`, `plans/`, `*.patch`, `.env`, `tests/`, `node_modules`, backup...
### 2.2 Frontend — chỉ ship bản precompiled
- Build JS: `node build.mjs` (Babel → `app.precompiled.js` minified) — **CHỈ bản minified vào image**, source `.jsx` bị `.dockerignore` chặn.
- Bump `?v=` (cache-bust) mỗi bản phát hành — user cập nhật không dính cache cũ.
### 2.3 Backend Python — giới hạn đọc source
- Python không biên dịch native mặc định → `.py` vẫn đọc được trong image. 3 lớp bảo vệ:
1. **Registry riêng tư** (GHCR / Docker Hub private / Harbor) — image KHÔNG public — người dùng chỉ nhận qua pull có auth.
2. **Secret chỉ qua env** — không hardcode gì trong image.
3. *(Tùy chọn, giai đoạn sau)* compile Python bằng **Nuitka**`.so` cho `app/core`, `app/api` (giữ `main.py`/`config.py` đọc được — không chứa bí mật).
### 2.4 Cách build + push
```bash
# 1. Precompile JS + bump ?v=
node build.mjs # hoặc: NODE=... babel ... (xem wiki)
# 2. Build image (tag theo version)
docker build -t ghcr.io/<org>/sonicforge-studio:v1.0.0-alpha.1 .
docker push ghcr.io/<org>/sonicforge-studio:v1.0.0-alpha.1
# 3. Production chạy image đã push (KHÔNG mount source):
docker compose -f docker-compose.prod.yml up -d
```
- Compose production (`docker-compose.prod.yml` — đã tạo): image từ registry, `env_file: .env`,
volumes bền (uploads/processed/db), asset dirs qua env (`VST3_DIR`, `SOUNDFONTS_DIR`, `PIANOBOOK_DIR`),
KHÔNG có `.:/app` (bỏ mount dev của compose cũ).
## 3. KẾ HOẠCH PHÁT HÀNH
### Giai đoạn 1 — ALPHA (nội bộ dev/QA)
- Tag: `v0.x.x-alpha.N` — chạy `docker compose up --build` (dev mount OK).
- Log verbose; chưa quan tâm bảo mật; test tính năng + thu hồi nhanh.
- Mỗi thay đổi: bump `?v=` → build → tag → ghi wiki.md.
### Giai đoạn 2 — BETA (nhóm người dùng mời)
- Tag: `v0.x.x-beta.N` — image push **registry riêng tư**.
- Người dùng: `docker compose -f docker-compose.prod.yml pull && up -d` (chỉ cần `.env` + image).
- Thu log lỗi: thêm endpoint `/health` + (tùy chọn) Sentry/self-host error tracking.
- **Feature flags** (biến env `FEATURE_*`) để tắt tính năng rủi ro từ xa.
### Giai đoạn 3 — PRODUCTION
- Tag: `vX.Y.Z` (semver) + `latest`.
- **Trước khi release:** backup volumes (`docker run --rm -v sf_db:/data -v $PWD:/backup alpine tar czf /backup/db-<ver>.tgz /data`), migrate dữ liệu nếu schema đổi.
- Triển khai: pull image mới → recreate (rolling — web trước, worker sau) → healthcheck.
- `SECRET_KEY`, `DEFAULT_ADMIN_PASSWORD`, API keys: quản lý qua secret manager (Docker secrets / vault) — không ở compose file.
## 4. PIPELINE CẬP NHẬT (lặp lại mỗi release)
```
code mới → build.mjs (precompiled + ?v=) → docker build (tag mới)
→ push registry → [beta/prod] pull + recreate → backup trước nếu prod → kiểm tra /health
```
- Phiên bản: `git tag vX.Y.Z` — build tag tự động từ git (`git describe --tags`).
- Rollback: giữ tag cũ — `docker compose -f docker-compose.prod.yml up -d` với `IMAGE=...:<tag cũ>`.
## 5. VIỆC CẦN LÀM (checklist)
- [x] `.env.example` — liệt kê đủ 7 biến
- [x] `.dockerignore` — chặn source/secret/docs
- [x] `docker-compose.prod.yml` — production (registry + env_file + volumes, bỏ mount source)
- [ ] Kiểm tra endpoint `/health` (tạo nếu chưa có — healthcheck compose đang trỏ tới)
- [ ] Chọn registry (GHCR/Docker Hub private/Harbor) + tạo token CI
- [ ] Nuitka compile `app/core`+`app/api` (tùy chọn — nếu cần bảo vệ backend chặt hơn)
- [ ] CI (GitHub Actions): test → build → push tag → deploy beta tự động
- [ ] Backup script volumes trước mỗi production release
+40
View File
@@ -0,0 +1,40 @@
MIT License
Copyright (c) 2026 SonicForge Studio contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## Third-party components
Code viết riêng của dự án (app/static/js/services/*, components/*, app.jsx)
thuộc license MIT trên.
Các thành phần bên thứ ba giữ license gốc:
- **React / react-dom** — MIT
- **Babel** (@babel/cli, core, preset-react) — MIT
- **jsdom** — MIT
- **Tailwind CSS** (app/static/css/tailwind.min.css) — MIT
- **Lucide icons** — ISC
- **FluidSynth 2.3.0** (app/static/js/vendor/libfluidsynth-2.3.0-sf3.js/.wasm) —
GNU LGPL v2.1+ — xem https://www.gnu.org/licenses/lgpl-2.1.html
Chi tiết + attribution từng thành phần: xem THIRD_PARTY_LICENSES.md.
+95
View File
@@ -0,0 +1,95 @@
# TEST & DEBUG PLAN — SonicForge DAW Standalone (Windows .exe / .msi)
Áp dụng cho bản đóng gói Tauri v2 + PyInstaller (xem `DESKTOP_INSTALL_PLAN.md`,
`build_windows.ps1`, `engine.spec`, `src-tauri/`).
---
## 0. Cổng kiểm soát TRƯỚC khi build (pre-build gates — chạy trên máy dev)
```powershell
# 1. Backend tests (repo đã có 86 tests pytest)
python -m pytest tests/ -x -q
# 2. Frontend syntax + bundle
node --check app/static/js/app.precompiled.js
node build.mjs # rebuild app.precompiled.js từ app.jsx
# 3. Python syntax của entry + spec
python -m py_compile desktop_engine.py engine.spec app/config.py app/tasks/worker.py
# 4. Chạy thử engine độc lập (chưa cần Tauri)
python desktop_engine.py # mở browser: http://127.0.0.1:8000/docs
```
Gate: tất cả PASS mới chạy `build_windows.ps1`.
---
## 1. TEST PLAN — theo 6 kịch bản của tài liệu gốc
| # | Kịch bản | Cách kiểm thử | Kết quả mong đợi |
|---|---|---|---|
| 1 | **Lifecycle & Process Spawn** | Mở app → Task Manager tìm `daw_engine.exe` → đóng cửa sổ → kiểm tra lại | Engine xuất hiện lúc launch; BIẾN MẤT hoàn toàn khi đóng (không orphan). Engine còn có watchdog: nếu cha chết đột ngột, tự thoát ≤1s |
| 2 | **Port 8000 & Fallback** | Mở app → browser truy cập `http://localhost:8000/docs`; hoặc chiếm sẵn port 8000 (vd `net stop` dịch vụ khác / chạy server khác) rồi mở app | Swagger UI hiển thị. Khi 8000 bị chiếm → engine tự chuyển 8001…8010, loader page trong app tự redirect tới port đúng |
| 3 | **VST3 Local Scan (Windows)** | Plugin Manager → set path `C:\Program Files\Common Files\VST3`**[Scan]** | Phát hiện đúng VST3 đã cài (Vital, Surge XT, FabFilter…). Scan state lưu ở `%APPDATA%\SonicForgeDAW\storage\sf_scan_state.json` |
| 4 | **Isolation & Crash Protection** | Load 1 VST3 không ổn định / giả lập crash | UI hiện thông báo lỗi, cửa sổ DAW vẫn phản hồi (tiến trình engine riêng — shell không chết theo) |
| 5 | **Storage Persistence (`%APPDATA%`)** | Mở Explorer tới `%APPDATA%\SonicForgeDAW\storage\` | Có `sonicforge.db`, `sf_scan_state.json`, `uploads/`, `processed/`, `soundfonts/`**không** nằm trong thư mục cài đặt (onefile giải nén tạm sẽ bị xóa khi thoát) |
| 6 | **Audio Export / Bouncing** | Click **Export WAV** ở chế độ Standalone | File `.wav` render đúng (VST3 + SoundFont qua Pedalboard/FluidSynth) |
### 1.1 Kiểm thử cài đặt (installer)
- **NSIS**: cài trên máy sạch (không có Python, không có VC++ redist) → app chạy được, `hooks.nsh` tự cài VC redist. Gỡ cài bằng Control Panel → không còn process, không còn shortcut.
- **MSI**: cài qua `msiexec /i SonicForgeDAW_1.0.0_x64_en-US.msi` (hoặc double-click) → tương đương; kiểm tra repair/uninstall.
- **Nâng cấp**: cài bản mới đè bản cũ → `%APPDATA%\SonicForgeDAW\storage` GIỮ NGUYÊN (dữ liệu người dùng không mất).
- **SmartScreen/AV**: lần đầu chạy có thể bị chặn ("More info → Run anyway"); bản production phải ký số (signtool + certificate DigiCert/Sectigo).
### 1.2 Kiểm thử hồi quy (regression — sau mỗi thay đổi code)
- `pytest tests/` — 86 test hiện có (conftest fixture dùng test client).
- Smoke UI thủ công: play/pause, piano roll, FX rack, save/load project, soundfont scan.
---
## 2. DEBUG WORKFLOWS
### 2.1 Engine (Python sidecar) không start / crash âm thầm
1. Mở log: `%APPDATA%\SonicForgeDAW\logs\engine.log` (desktop_engine.py ghi log ra file vì `console=False`).
2. Chạy thủ công engine có console: sửa `console=True` trong `engine.spec` → rebuild → chạy `dist\daw_engine.exe` từ cmd → xem traceback trực tiếp.
3. Test độc lập không cần Tauri: `python desktop_engine.py` (dev) — đúng code path, có stdout.
### 2.2 UI (WebView2)
- **F12 / Ctrl+Shift+I** trong cửa sổ app: mở DevTools (đã bật `"devtools": true` trong `tauri.conf.json`) — Console, Network, Web Audio context.
- `devUrl: "http://localhost:8000"` — bản dev có thể chạy `tauri dev` với engine chạy tay.
### 2.3 Sidecar không spawn / không thấy engine
- Kiểm tra `src-tauri\binaries\daw_engine-x86_64-pc-windows-msvc.exe` có tồn tại (build script copy tự động).
- Chạy `npx tauri build` lần đầu: nếu thiếu sidecar → lib.rs `expect("sidecar daw_engine not found")` fail rõ ràng.
- Kiểm tra port bị chiếm: `netstat -ano | findstr :8000`.
### 2.4 Windows build lỗi (máy dev)
- Rust toolchain: `rustup default stable-msvc` (bắt buộc MSVC toolchain cho Tauri Windows).
- WebView2 runtime: Win10/11 có sẵn; máy cũ cài [WebView2 Evergreen](https://developer.microsoft.com/microsoft-edge/webview2/).
- Nếu `tauri build` lỗi NSIS/MSI riêng: build riêng từng target `npx tauri build --bundles nsis` / `--bundles msi`.
---
## 3. Known Gaps (đã xử lý / chấp nhận)
| Vấn đề | Trạng thái |
|---|---|
| Celery cần Redis broker | **Đã xử lý**: `SF_DESKTOP=1``task_always_eager=True` (chạy đồng bộ trong tiến trình, `memory://` broker + `cache+memory://` backend). Chế độ docker/cloud không đổi |
| Storage khi frozen (onefile giải nén tạm) | **Đã xử lý**: `config.py` freeze-aware → writable data về `%APPDATA%\SonicForgeDAW\storage`, assets read-only trong `_MEIPASS` |
| Port 8000 bị chiếm | **Đã xử lý**: probe 80008010 + loader page redirect tự động |
| `engine.spec` console=False → mù log | **Đã xử lý**: log ra file `%APPDATA%\SonicForgeDAW\logs\engine.log` |
| Soundfont scanner path `/opt/daw_engine/soundfonts` (Linux-only) | **Chấp nhận**: cần xác minh scanner bỏ qua path không tồn tại trên Windows (test scenario #5) |
| `child.kill()` = TerminateProcess (không graceful 100%) | **Giảm thiểu**: watchdog parent-PID trong `desktop_engine.py` → engine tự shutdown sạch khi cha chết |
---
## 4. Checklist phát hành (release)
- [ ] Pre-build gates PASS (mục 0)
- [ ] `build_windows.ps1` chạy sạch, đủ 6 bước
- [ ] 6 kịch bản test (mục 1) PASS trên máy sạch
- [ ] Installer NSIS + MSI cài/gỡ/nâng cấp đúng
- [ ] Ký số (production) — SmartScreen hết cảnh báo
- [ ] Commit artifacts: `engine.spec`, `desktop_engine.py`, `src-tauri/`, `build_windows.ps1`, `tools/gen_icons.py`
+37
View File
@@ -0,0 +1,37 @@
# THIRD-PARTY LICENSES — SonicForgeStudio
Kiểm tra ngày 2026-08-07. Danh sách thành phần bên thứ ba + license.
## 1. Dependencies (npm — package.json)
| Thành phần | License | Ghi chú |
|---|---|---|
| react / react-dom 19.x | MIT | OK |
| @babel/cli, @babel/core, @babel/preset-react | MIT | Chỉ build-time |
| jsdom | MIT | Chỉ build-time/test |
## 2. Thư viện nhúng (app/static)
| File | Nguồn | License | Trạng thái |
|---|---|---|---|
| `css/tailwind.min.css` | Tailwind CSS | MIT | OK — nên giữ attribution |
| `js/services/spessasynth_processor.min.js` | SpessaSynth (KHÔNG còn dùng — không được include trong index.html — chỉ FluidSynth) | MIT | ✅ KHÔNG có code nào gọi (chỉ còn comment cũ app.jsx:14276 + file chết) — có thể xóa file |
| `js/vendor/libfluidsynth-2.3.0-sf3.js` + `.wasm` | FluidSynth 2.3.0 (Emscripten) | **LGPL-2.1+** | ⚠️ Bắt buộc giữ license notice + attribution + (nếu phân phối bản build) cung cấp link/tài liệu LGPL |
| `js/services/fluidsynthLoader.js`, `worklets/fluidsynth-bridge.js` | Bản tự viết (bọc FluidSynth) | Dự án | OK |
| Lucide icons (inline `data-lucide`) | Lucide | ISC (MIT-compatible) | OK |
## 3. Nội dung âm thanh (app/storage)
| File | Nguồn | License | Trạng thái |
|---|---|---|---|
| `soundfonts/518e850f-...sf2` | "General MIDI SoundFont v3.0" — © 2006-2010 Rich "Weeds" Nagel — "Some rights reserved" | Không có text đầy đủ trong file (chỉ ICOP ngắn). Tuyên bố: "created from various **commercial**, custom, and freeware soundfonts and samples" | ⚠️ RỦI RO: (a) điều kiện "some rights reserved" không rõ ràng (thường là CC-BY — cần ghi attribution; có thể hạn chế thương mại); (b) samples gốc có nguồn commercial — quyền tái phân phối phụ thuộc tuyên bố tác giả. **Khuyến nghị: thay bằng SF2 license rõ (FluidR3_GM — MIT/GPL-2; GeneralUser GS — CC-BY-SA; Arachno — public domain) HOẶC giữ + ghi attribution đầy đủ.** |
| `uploads/user_anonymous_*.mp3` | File người dùng upload | Thuộc người dùng | OK — không phải thành phần phân phối |
| SGM-V2.01 (xuất hiện trong log `sfId: SGM-V2.01`) | SGM-V2.01 | Freeware — tác giả cho phép dùng nhưng **hạn chế redistribution** (cần permission) | ⚠️ Nếu vẫn dùng để phân phối bản build — cần permission từ tác giả; không nhúng vào sản phẩm |
## 4. Assets
- `templates/favicon.svg`, `images/SonicForgeUI.png` — nội bộ (tự tạo) — OK.
- Code trong `app/static/js/services/*`, `app/static/js/components/*`, `app.jsx` — tự viết — thuộc dự án.
## KẾT LUẬN
- **Không phát hiện vi phạm bản quyền rõ ràng** (không có code GPL bị nhúng vào project MIT; LGPL của FluidSynth tương thích nếu giữ notice).
- **3 điểm cần xử lý trước khi phân phối công khai:**
1. Bổ sung `LICENSE` text (MIT + LGPL-2.1) + attribution cho SpessaSynth (MIT notice) và Tailwind.
2. Ghi attribution SF2 Rich Nagel ("General MIDI SoundFont v3.0 — © 2006-2010 Rich 'Weeds' Nagel — Some rights reserved") hoặc thay soundfont khác license rõ.
3. Không nhúng SGM-V2.01 vào bản phân phối (hạn chế redistribution).
+29
View File
@@ -259,3 +259,32 @@ async def get_profile(current_user: dict = Depends(get_current_user)):
"max_tracks": row["max_tracks"] or 16
}
}
@router.get("/first-time")
async def auth_first_time():
# "Lần đăng nhập đầu" = tài khoản admin vẫn dùng mật khẩu MẶC ĐỊNH
# (chưa từng đổi). Sau khi đổi lần đầu → first_time = false → UI xóa
# gợi ý username/mật khẩu (Tùy chọn - Admin có thể bỏ trống, lần đầu:
# admin123, nút Điền nhanh).
try:
conn = get_db_connection()
try:
cur = conn.cursor()
cur.execute(
"SELECT id, hashed_password, must_change_password FROM users "
"WHERE LOWER(role) = 'admin' ORDER BY created_at ASC LIMIT 1"
)
row = cur.fetchone()
finally:
conn.close()
if not row:
return {"first_time": True}
still_default = False
try:
still_default = verify_password("admin123", row["hashed_password"])
except Exception:
still_default = False
return {"first_time": bool(row["must_change_password"]) and still_default}
except Exception:
return {"first_time": True}
+19 -3
View File
@@ -1,15 +1,31 @@
import os
import sys
def _app_dir():
if getattr(sys, "frozen", False):
# PyInstaller onefile: assets read-only nằm trong thư mục giải nén tạm
return os.path.join(sys._MEIPASS, "app")
return os.path.dirname(os.path.abspath(__file__))
def _storage_dir():
if getattr(sys, "frozen", False):
# Dữ liệu ghi được (DB, uploads, processed) phải ngoài thư mục tạm
root = os.environ.get("APPDATA") or os.path.expanduser("~")
return os.path.join(root, "SonicForgeDAW", "storage")
return os.path.join(_app_dir(), "storage")
class Settings:
REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379/0")
CELERY_BROKER_URL: str = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0")
CELERY_RESULT_BACKEND: str = os.getenv("CELERY_RESULT_BACKEND", "redis://localhost:6379/0")
# __file__ is app/config.py, so dirname(__file__) = app/, dirname(app/) = project root
APP_DIR: str = os.path.dirname(os.path.abspath(__file__))
APP_DIR: str = _app_dir()
BASE_DIR: str = os.path.dirname(APP_DIR)
TEMPLATES_DIR: str = os.path.join(APP_DIR, "templates")
STORAGE_DIR: str = os.path.join(APP_DIR, "storage")
STORAGE_DIR: str = _storage_dir()
UPLOADS_DIR: str = os.path.join(STORAGE_DIR, "uploads")
PROCESSED_DIR: str = os.path.join(STORAGE_DIR, "processed")
+25 -1
View File
@@ -58,7 +58,27 @@ app.add_middleware(
# Mount storage directory (must come before general /static mount)
app.mount("/static/audio", StaticFiles(directory=settings.STORAGE_DIR), name="audio")
# Mount app static files (js, css)
STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
# Dùng settings.APP_DIR (freeze-aware) thay vì os.path.dirname(__file__):
# khi PyInstaller onefile, __file__ trỏ vào thư mục giải nén tạm _MEI...
# nhưng static/templates nằm trong sys._MEIPASS/app (config.py đã xử lý).
STATIC_DIR = os.path.join(settings.APP_DIR, "static")
# ⚠️ Fallback an toàn: nếu vì lý do nào đó static không nằm đúng chỗ (vd
# bundle thiếu file, chạy từ nơi khác), thử các vị trí khác; nếu vẫn không
# có → TỰ TẠO thư mục rỗng để app KHÔNG crash khi khởi động (lỗi "Directory
# does not exist" từ StaticFiles làm engine chết ngay lúc import — đã gặp).
if not os.path.isdir(STATIC_DIR):
for cand in [
os.path.join(getattr(sys, "_MEIPASS", ""), "app", "static"),
os.path.join(os.path.dirname(os.path.abspath(__file__)), "static"),
]:
if cand and os.path.isdir(cand):
STATIC_DIR = cand
break
else:
try:
os.makedirs(STATIC_DIR, exist_ok=True)
except OSError:
pass
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
# Include routers
@@ -75,6 +95,10 @@ app.include_router(plugins_router, prefix="/api/v1/plugins", tags=["plugins"])
app.include_router(media_router, prefix="/api/v1/media", tags=["media"])
@app.get("/health")
async def health():
return {"status": "ok"}
@app.get("/", response_class=HTMLResponse)
async def get_index():
index_path = os.path.join(settings.TEMPLATES_DIR, "index.html")
+2217 -344
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -84,6 +84,11 @@
if (dest === _outputDestination) return;
_gainNode.disconnect();
_gainNode.connect(dest);
// Reconnect VU analyser tap (app.jsx VU tick gắn __vuAnalyser
// trên _gainNode): disconnect() không đối số ngắt MỌI kết
// nối kể cả analyser → sfAudioPeak = 0 mãi → track VU MIDI
// không nhảy khi có âm (user bug 06:35).
if (_gainNode.__vuAnalyser) _gainNode.connect(_gainNode.__vuAnalyser);
_outputDestination = dest;
console.log('[SonicSF] setOutputDestination to:', node ? 'track node (sfEntry)' : 'masterBus.input');
} else {
+10
View File
@@ -27,6 +27,16 @@ celery_app.conf.update(
enable_utc=True,
)
# Che do desktop (SF_DESKTOP=1, do desktop_engine.py set): chay task dong bo
# trong tien trinh (eager) — ban Standalone Windows KHONG kem Redis broker.
if os.getenv("SF_DESKTOP") == "1":
celery_app.conf.update(
task_always_eager=True,
task_eager_propagates=True,
broker_url="memory://",
result_backend="cache+memory://",
)
# ── Lịch trình tự động dọn dẹp file hết hạn (Week 5) ──
celery_app.conf.beat_schedule = {
"cleanup-expired-files-every-hour": {
+57 -3
View File
@@ -3,8 +3,27 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>SonicForge Studio - Professional DAW Editor</title>
<script>
// ── Chặn BROWSER ZOOM toàn trang ──
// UI dùng px cố định cho item/button/label — browser zoom (Ctrl+wheel,
// Ctrl+plus/minus/0, pinch) scale TOÀN BỘ làm control phóng to theo.
// Chặn ở capture phase bằng preventDefault() (KHÔNG stopPropagation —
// các vùng zoom chuyên dụng: timeline, piano roll, canvas, EQ vẫn nhận
// event và tự xử lý zoom nội dung của chúng).
document.addEventListener('wheel', function (e) {
if (e.ctrlKey || e.metaKey) e.preventDefault();
}, { capture: true, passive: false });
document.addEventListener('keydown', function (e) {
if ((e.ctrlKey || e.metaKey) && ['+', '-', '=', '_', '0'].indexOf(e.key) !== -1) {
e.preventDefault();
}
}, { capture: true });
// Pinch zoom (Safari/WebKit gesture events)
document.addEventListener('gesturestart', function (e) { e.preventDefault(); }, { passive: false });
document.addEventListener('gesturechange', function (e) { e.preventDefault(); }, { passive: false });
</script>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
@@ -16,7 +35,7 @@
<script src="/static/js/services/audioEngine.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=202608060630"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608070635"></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>
@@ -24,7 +43,7 @@
<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=202608061030" defer></script>
<script src="/static/js/app.precompiled.js?v=202608081600" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
@@ -33,8 +52,43 @@
--top-bar-height: 80px;
--status-bar-height: 25px;
--panel-border-color: #2a2a2a;
/* Theme (Preferences) — có thể override bằng data-theme trên <html> */
--sf-bg: #1e1e1e;
--sf-panel: #262626;
--sf-header: #2e2e2e;
--sf-border: #181818;
--sf-accent: #00ffcc;
}
/* ── THEME presets (Tools → Preferences) ── */
html[data-theme="dark"] {
--sf-bg: #1e1e1e; --sf-panel: #262626; --sf-header: #2e2e2e; --sf-border: #181818; --sf-accent: #00ffcc;
}
html[data-theme="midnight"] {
--sf-bg: #0f172a; --sf-panel: #1e293b; --sf-header: #1e293b; --sf-border: #0f172a; --sf-accent: #38bdf8;
}
html[data-theme="forest"] {
--sf-bg: #111c15; --sf-panel: #1b2a1e; --sf-header: #1e2d21; --sf-border: #0c1510; --sf-accent: #34d399;
}
html[data-theme="violet"] {
--sf-bg: #170f26; --sf-panel: #221537; --sf-header: #251640; --sf-border: #110a1c; --sf-accent: #a78bfa;
}
html[data-theme="graphite"] {
--sf-bg: #18181b; --sf-panel: #232327; --sf-header: #27272a; --sf-border: #101012; --sf-accent: #e4e4e7;
}
/* Áp theme lên các vùng chính của app shell */
.daw-app-shell { background-color: var(--sf-bg) !important; }
.daw-panel { background-color: var(--sf-panel) !important; }
.daw-header { background-color: var(--sf-header) !important; }
body { background-color: var(--sf-bg) !important; }
::-webkit-scrollbar-thumb { background: var(--sf-accent); }
/* ── BUTTON FONT SIZE (Tools → Preferences) ── */
html[data-btnfont="sm"] button { font-size: 10px !important; }
html[data-btnfont="md"] button { font-size: 12px !important; }
html[data-btnfont="lg"] button { font-size: 14px !important; }
body {
background-color: #1a1a1a;
color: #c0c0c0;
+62
View File
@@ -0,0 +1,62 @@
# build_windows.ps1 - ONE-COMMAND build: daw_engine.exe (PyInstaller) + Tauri v2 (NSIS + MSI)
# Chay tren Windows: powershell -ExecutionPolicy Bypass -File build_windows.ps1
# LUU Y: file nay chi dung ky tu ASCII (khong dau, khong em-dash) - PowerShell 5.1
# doc .ps1 khong BOM theo ANSI, ky tu Unicode bi hong -> "String is missing terminator".
$ErrorActionPreference = "Stop"
Set-Location $PSScriptRoot
Write-Host "== [1/6] Python dependencies =="
python -m pip install --upgrade pip
python -m pip install -r requirements.txt pyinstaller pywin32
Write-Host "== [2/6] Frontend bundle (app.jsx -> app.precompiled.js) =="
npm install
# Dam bao @babel/standalone co (build.mjs import truc tiep - da gap
# ERR_MODULE_NOT_FOUND tren may Windows khi package.json cu thieu dep).
if (-not (Test-Path "node_modules\@babel\standalone")) {
Write-Host "Thieu @babel/standalone - dang cai them..."
npm install @babel/standalone --no-audit --no-fund
}
if (-not (Test-Path "node_modules\@babel\standalone")) {
Write-Host "ERROR: Khong cai duoc @babel/standalone. Kiem tra ket noi npm!" -ForegroundColor Red
exit 1
}
node build.mjs # @babel/standalone; thay cho 'npm run build' (Babel 8 ESM-only CLI conflict)
Write-Host "== [3/6] Build daw_engine.exe (PyInstaller) =="
pyinstaller engine.spec --clean --noconfirm
Write-Host "== [3.5/6] Verify bundle contents (app/static, app/templates phai co) =="
# Bat loi bundle NGAY tai build (da gap 2 lan: chay pyinstaller tu noi khac
# -> static/templates thieu -> exe crash 'Directory ...\app\static does not exist').
# Dung tools/verify_bundle.py (parse TOC bang ast, chap nhan ca / va \) thay
# vi regex thong thuong (TOC Windows co the dung backslash -> false positive).
# Truoc tien kiem tra engine.spec phai la ban moi (collect_data_files).
$specContent = Get-Content "engine.spec" -Raw -ErrorAction SilentlyContinue
if ($specContent -notmatch "collect_data_files\('app'") {
Write-Host "ERROR: engine.spec CU (thieu collect_data_files('app')). Pull code moi truoc khi build!" -ForegroundColor Red
exit 1
}
python tools\verify_bundle.py
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: Bundle thieu asset - dung build, kiem tra engine.spec datas!" -ForegroundColor Red
exit 1
}
Write-Host "== [4/6] Sidecar binary -> src-tauri/binaries (tauri triple naming) =="
New-Item -ItemType Directory -Force src-tauri\binaries | Out-Null
Copy-Item dist\daw_engine.exe src-tauri\binaries\daw_engine-x86_64-pc-windows-msvc.exe -Force
Write-Host "== [5/6] VC++ Redistributable cho hooks.nsh =="
if (-not (Test-Path src-tauri\vc_redist.x64.exe)) {
Invoke-WebRequest -Uri "https://aka.ms/vs/17/release/vc_redist.x64.exe" -OutFile src-tauri\vc_redist.x64.exe
}
Write-Host "== [6/6] Tauri build (NSIS .exe + MSI) =="
npm install -D @tauri-apps/cli
npx tauri build
Write-Host ""
Write-Host "== DONE =="
Write-Host " NSIS: src-tauri\target\release\bundle\nsis\SonicForgeDAW_1.0.0_x64-setup.exe"
Write-Host " MSI : src-tauri\target\release\bundle\msi\SonicForgeDAW_1.0.0_x64_en-US.msi"
+87
View File
@@ -0,0 +1,87 @@
"""SonicForge DAW — Desktop engine launcher (PyInstaller entry point).
Chay FastAPI backend duoi dang tien trinh nen (sidecar) cho ban cai Standalone
Windows (Tauri v2 shell). Tu chon port 8000-8010 (fallback neu bi chiem), tu
thoat khi tien trinh cha (Tauri) ket thuc Graceful Shutdown.
"""
import logging
import os
import socket
import sys
import threading
import time
PORT_RANGE = (8000, 8010)
APP_DATA_DIR_NAME = "SonicForgeDAW"
def _pick_port():
for port in range(PORT_RANGE[0], PORT_RANGE[1] + 1):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
s.bind(("127.0.0.1", port))
return port
except OSError:
continue
return PORT_RANGE[0]
def _parent_alive(pid):
try:
os.kill(pid, 0)
return True
except OSError:
return False
def main():
os.environ.setdefault("SF_DESKTOP", "1")
port = _pick_port()
os.environ["SF_PORT"] = str(port)
# Log ra file — khi dong goi console=False, stdout khong nhin thay duoc.
try:
log_dir = os.path.join(
os.environ.get("APPDATA") or os.path.expanduser("~"),
APP_DATA_DIR_NAME, "logs",
)
os.makedirs(log_dir, exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
handlers=[
logging.FileHandler(os.path.join(log_dir, "engine.log"), encoding="utf-8"),
logging.StreamHandler(),
],
)
except Exception:
logging.basicConfig(level=logging.INFO)
import uvicorn
import app.main # noqa: F401 — import tuong minh de PyInstaller bundle du package app
config = uvicorn.Config(app.main.app, host="127.0.0.1", port=port,
log_level="info", access_log=False)
server = uvicorn.Server(config)
parent_pid = os.environ.get("SF_PARENT_PID")
if parent_pid:
def _watchdog():
pid = int(parent_pid)
while True:
time.sleep(1.0)
if not _parent_alive(pid):
logging.getLogger("desktop_engine").info(
"Parent process exited — shutting down engine")
server.should_exit = True
return
threading.Thread(target=_watchdog, daemon=True).start()
logging.getLogger("desktop_engine").info(
"SonicForge engine listening on 127.0.0.1:%d", port)
server.run()
if __name__ == "__main__":
main()
+58
View File
@@ -0,0 +1,58 @@
# ── Production compose — KHÔNG mount source, image từ registry, bí mật qua .env ──
# Dùng: docker compose -f docker-compose.prod.yml up -d
services:
redis:
image: redis:7-alpine
restart: unless-stopped
web:
image: ${IMAGE:-sonicforge-studio:latest}
restart: unless-stopped
ports:
- "${WEB_PORT:-8000}:8000"
env_file: .env
volumes:
- sf_uploads:/app/app/storage/uploads
- sf_processed:/app/app/storage/processed
- sf_db:/app/app/storage
- ${VST3_DIR:-./vst_plugins}:/opt/daw_engine/vst3:ro
- ${SOUNDFONTS_DIR:-./soundfonts}:/opt/daw_engine/soundfonts:ro
- ${PIANOBOOK_DIR:-./samples/pianobook}:/opt/daw_engine/samples/pianobook:ro
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request;urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
depends_on:
- redis
worker:
image: ${IMAGE:-sonicforge-studio:latest}
restart: unless-stopped
command: celery -A app.tasks.worker.celery_app worker --loglevel=info
env_file: .env
volumes:
- sf_uploads:/app/app/storage/uploads
- sf_processed:/app/app/storage/processed
- sf_db:/app/app/storage
- ${VST3_DIR:-./vst_plugins}:/opt/daw_engine/vst3:ro
- ${SOUNDFONTS_DIR:-./soundfonts}:/opt/daw_engine/soundfonts:ro
- ${PIANOBOOK_DIR:-./samples/pianobook}:/opt/daw_engine/samples/pianobook:ro
depends_on:
- redis
beat:
image: ${IMAGE:-sonicforge-studio:latest}
restart: unless-stopped
command: celery -A app.tasks.worker.celery_app beat --loglevel=info
env_file: .env
volumes:
- sf_db:/app/app/storage
depends_on:
- redis
volumes:
sf_uploads:
sf_processed:
sf_db:
+107
View File
@@ -0,0 +1,107 @@
# engine.spec — PyInstaller config cho daw_engine.exe (sidecar Python)
# Chay: pyinstaller engine.spec --clean --noconfirm (tren Windows)
# -*- mode: python ; coding: utf-8 -*-
import os
from PyInstaller.utils.hooks import collect_dynamic_libs, collect_submodules, collect_data_files
# Native DLL cho pedalboard va soundfile
binaries = collect_dynamic_libs('pedalboard')
binaries += collect_dynamic_libs('soundfile')
# Root tuyet doi cua thu muc chua spec — dung cho cac datas KHONG nam trong
# package 'app' (vd thu muc md/). Cac assets cua app (static/templates/models)
# duoc bundle qua collect_data_files('app').
_SPEC_ROOT = os.path.abspath(SPECPATH)
# ⚠️ GOC ROOT CUA MOI LOI 'app\static does not exist' (gap 3 lan):
# lenh `pyinstaller engine.spec` (entry-point script) KHONG them CWD vao
# sys.path (chi `python -m PyInstaller` moi them). collect_data_files('app')
# import package qua sys.path -> khong thay 'app' -> tra ve [] AM THAM ->
# bundle thieu static/templates -> exe crash luc chay. Fix: chen _SPEC_ROOT
# vao sys.path TRUOC khi collect de import 'app' luon hoạt dong.
import sys as _sys
if _SPEC_ROOT not in _sys.path:
_sys.path.insert(0, _SPEC_ROOT)
# Assets cua app: bundle QUA IMPORT SYSTEM (collect_data_files) — an toan nhat.
# Loai tru storage (57MB soundfonts/uploads — vo ich trong onefile, config.py
# da chuyen storage sang %APPDATA%\\SonicForgeDAW khi frozen) va __pycache__.
datas = collect_data_files('app', excludes=['**/storage/**', '**/__pycache__/**', '**/*.pyc'])
# Fallback cuoi cung: neu collect_data_files van tra ve rong (phong moi truong
# hop ky la), dung datas TINH absolute — tinh huong xau nhat van co du assets.
if not datas:
print("WARN: collect_data_files('app') tra ve rong - dung datas tinh absolute")
datas = [
(os.path.join(_SPEC_ROOT, 'app', 'templates'), 'app/templates'), # index.html, favicon.svg
(os.path.join(_SPEC_ROOT, 'app', 'static'), 'app/static'), # js/css/processors
(os.path.join(_SPEC_ROOT, 'app', 'models'), 'app/models'), # project_schema.json (projects.py)
]
datas += [
(os.path.join(_SPEC_ROOT, 'md'), 'md'), # /ai-prompt-generator (doc, ngoai package app)
]
# librosa 0.11 dùng lazy_loader.attach_stub -> lúc RUNTIME cần file .pyi
# ton tai tren disk ('Cannot load imports from non-existent stub ...librosa\__init__.pyi').
# PyInstaller mac dinh KHONG bundle .pyi -> phai collect explicit.
datas += collect_data_files('librosa', includes=['**/*.pyi'])
# scipy >= 1.18 tach scipy.stats thanh nhieu module con (vd
# _ansari_swilk_statistics) import lazy ben trong ham -> hook scipy cua
# PyInstaller miss -> ModuleNotFoundError luc runtime. Collect toan bo
# submodules cua scipy de khong sot module nao (stats/signal/ndimage...).
_scipy_hidden = collect_submodules('scipy')
# Cung co bang hiddenimport TINH: _morestats import module nay o top-level
# (scipy 1.18+); collect_submodules du phong nhung neu miss (version khac
# tren may user) thi dong nay van dam bao bundle co.
if 'scipy.stats._ansari_swilk_statistics' not in _scipy_hidden:
_scipy_hidden.append('scipy.stats._ansari_swilk_statistics')
a = Analysis(
['desktop_engine.py'],
pathex=[_SPEC_ROOT],
binaries=binaries,
datas=datas,
hiddenimports=collect_submodules('celery.fixups') + _scipy_hidden + [
'uvicorn.logging',
'uvicorn.loops',
'uvicorn.loops.auto',
'uvicorn.protocols',
'uvicorn.protocols.http',
'uvicorn.protocols.http.auto',
'uvicorn.protocols.websockets',
'uvicorn.protocols.websockets.auto',
'pedalboard',
'soundfile',
'sf2utils',
'mido.backends.rtmidi',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=['tkinter'],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=None)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='daw_engine',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False, # True khi debug (xem log truc tiep), False cho production
icon='src-tauri/icons/icon.ico',
)
+10
View File
@@ -9,6 +9,7 @@
"@babel/cli": "^8.0.4",
"@babel/core": "^8.0.1",
"@babel/preset-react": "^8.0.1",
"@babel/standalone": "^7.29.8",
"jsdom": "^30.0.1",
"react": "^19.2.8",
"react-dom": "^19.2.8"
@@ -355,6 +356,15 @@
"@babel/core": "^8.0.0"
}
},
"node_modules/@babel/standalone": {
"version": "7.29.8",
"resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.29.8.tgz",
"integrity": "sha512-XgbPNz+u6JzB7cKGnPDoS1U24J5td8yp3HFWbT/6f4/ASZ0PqaQGIvts1o5v5AI+f6i3jdVB/L/o2CYLCv101A==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz",
+1
View File
@@ -8,6 +8,7 @@
"@babel/cli": "^8.0.4",
"@babel/core": "^8.0.1",
"@babel/preset-react": "^8.0.1",
"@babel/standalone": "^7.29.8",
"jsdom": "^30.0.1",
"react": "^19.2.8",
"react-dom": "^19.2.8"
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "sonicforge-daw"
version = "1.0.0"
description = "SonicForge DAW desktop shell (Tauri v2 + Python sidecar)"
edition = "2021"
[lib]
name = "sonicforge_daw_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[profile.release]
strip = true
lto = true
codegen-units = 1
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capability for the main window",
"windows": ["main"],
"permissions": ["core:default"]
}
+7
View File
@@ -0,0 +1,7 @@
; NSIS installer hooks cai Microsoft Visual C++ Redistributable neu thieu
; (MSVCP140.dll missing). Dat vc_redist.x64.exe CANH tauri.conf.json
; (build_windows.ps1 tu tai ve neu chua co).
!macro customInstall
File "/oname=$PLUGINSDIR\vc_redist.x64.exe" "vc_redist.x64.exe"
ExecWait '"$PLUGINSDIR\vc_redist.x64.exe" /install /quiet /norestart'
!macroend
Binary file not shown.

After

Width:  |  Height:  |  Size: 1009 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

+46
View File
@@ -0,0 +1,46 @@
// SonicForge DAW — desktop shell: spawn/terminate daw_engine.exe sidecar.
use tauri::Manager;
use tauri_plugin_shell::process::CommandChild;
use tauri_plugin_shell::ShellExt;
use std::sync::Mutex;
struct EngineProcess(Mutex<Option<CommandChild>>);
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.setup(|app| {
// 1. Spawn sidecar daw_engine.exe (PyInstaller bundle)
let sidecar_command = app
.shell()
.sidecar("daw_engine")
.expect("sidecar daw_engine not found — run build_windows.ps1 first");
let (_rx, child) = sidecar_command
.env("SF_PARENT_PID", std::process::id().to_string())
.spawn()
.expect("Failed to spawn daw_engine sidecar");
app.manage(EngineProcess(Mutex::new(Some(child))));
println!("Python Background Engine started (localhost:8000, auto-fallback 8000-8010)");
Ok(())
})
.on_window_event(|window, event| {
// 2. Terminate sidecar khi DAW window dong — tranh orphan process
if let tauri::WindowEvent::Destroyed = event {
// Lay child ra khoi lock, guard drop ngay tai day (trach E0597)
let child = window
.state::<EngineProcess>()
.0
.lock()
.ok()
.and_then(|mut lock| lock.take());
if let Some(child) = child {
let _ = child.kill();
println!("daw_engine sidecar terminated.");
}
}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+5
View File
@@ -0,0 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
sonicforge_daw_lib::run()
}
+43
View File
@@ -0,0 +1,43 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "SonicForgeDAW",
"version": "1.0.0",
"identifier": "com.sonicforge.daw",
"build": {
"frontendDist": "ui",
"devUrl": "http://localhost:8000"
},
"app": {
"windows": [
{
"title": "Sonic Forge DAW - Professional Desktop Studio",
"width": 1440,
"height": 900,
"resizable": true,
"fullscreen": false,
"devtools": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": ["msi", "nsis"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.ico"
],
"externalBin": [
"binaries/daw_engine"
],
"windows": {
"nsis": {
"installerHooks": "hooks.nsh"
}
}
}
}
+35
View File
@@ -0,0 +1,35 @@
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8">
<title>SonicForge DAW</title>
<style>
body { font-family: system-ui, sans-serif; background: #12141a; color: #e8eaf0;
display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
#msg { text-align: center; max-width: 480px; line-height: 1.6; }
</style>
</head>
<body>
<div id="msg">Đang khởi động SonicForge Engine…</div>
<script>
// Engine (sidecar) có thể cần vài giây để boot (import librosa/pedalboard).
// Thử health-check trên dải port 8000-8010, redirect tới port đầu tiên OK.
(async function () {
var tries = 0;
while (tries < 90) { // ~60s tối đa
for (var p = 8000; p <= 8010; p++) {
try {
var r = await fetch('http://127.0.0.1:' + p + '/health', { cache: 'no-store' });
if (r.ok) { location.href = 'http://127.0.0.1:' + p + '/'; return; }
} catch (e) { /* port chưa mở */ }
}
tries++;
await new Promise(function (res) { setTimeout(res, 700); });
}
document.getElementById('msg').textContent =
'Không tìm thấy SonicForge Engine (port 80008010). Hãy đóng và chạy lại ứng dụng. ' +
'Nếu lặp lại, xem log: %APPDATA%\\SonicForgeDAW\\logs\\engine.log';
})();
</script>
</body>
</html>
+59
View File
@@ -0,0 +1,59 @@
"""Sinh bo icon cho Tauri v2 + PyInstaller (chay 1 lan, co the tai chay).
Usage: python tools/gen_icons.py
Output: src-tauri/icons/{32x32,128x128,128x128@2x}.png, icon.png, icon.ico
"""
import os
from PIL import Image, ImageDraw, ImageFont
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ICONS = os.path.join(ROOT, "src-tauri", "icons")
BG = (18, 20, 26, 255) # #12141a — trùng màu nền loader
FG = (77, 182, 255, 255) # xanh dương sáng
RING = (46, 52, 66, 255)
def _font(size):
for p in ("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"C:/Windows/Fonts/segoeuib.ttf"):
if os.path.exists(p):
return ImageFont.truetype(p, size)
return ImageFont.load_default()
def _draw(size):
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
d = ImageDraw.Draw(img)
# nền bo góc + viền
r = size // 8
d.rounded_rectangle([0, 0, size - 1, size - 1], radius=r, fill=BG)
d.rounded_rectangle([size // 32, size // 32, size - 1 - size // 32, size - 1 - size // 32],
radius=max(1, r - size // 32), outline=RING, width=max(2, size // 64))
# chữ "SF"
font = _font(int(size * 0.42))
text = "SF"
bbox = d.textbbox((0, 0), text, font=font)
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
d.text(((size - w) / 2 - bbox[0], (size - h) / 2 - bbox[1]), text,
font=font, fill=FG)
return img
def main():
os.makedirs(ICONS, exist_ok=True)
sizes = {"32x32.png": 32, "128x128.png": 128, "128x128@2x.png": 256, "icon.png": 512}
imgs = []
for name, size in sizes.items():
img = _draw(size)
img.save(os.path.join(ICONS, name))
imgs.append((img, size))
print("generated", name)
# icon.ico: multi-size
base = _draw(256)
base.save(os.path.join(ICONS, "icon.ico"),
sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)])
print("generated icon.ico")
if __name__ == "__main__":
main()
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Verify PyInstaller bundle TOC chua du app assets (app/static, app/templates).
Dung boi build_windows.ps1 (buoc 3.5/6) de bat loi bundle NGAY tai build:
python tools/verify_bundle.py
Exit code 0 = OK, 1 = thieu asset.
Ly do ton tai: da gap 2 lan exe crash 'Directory ...\\app\\static does not exist'
vi chay pyinstaller tu noi khac -> datas relative khong resolve -> bo qua am tham.
"""
import glob
import os
import re
import sys
REQUIRED = ["app/static", "app/templates"]
def find_toc(build_dir: str) -> str:
# PyInstaller 6.x: build/engine/Analysis-00.toc (hoac *-00.toc khac)
for pattern in (
os.path.join(build_dir, "engine", "Analysis-00.toc"),
os.path.join(build_dir, "engine", "*-00.toc"),
os.path.join(build_dir, "**", "Analysis-00.toc"),
os.path.join(build_dir, "**", "*-00.toc"),
):
hits = glob.glob(pattern, recursive=True)
if hits:
return hits[0]
return ""
def main() -> int:
build_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build"))
toc = find_toc(build_dir)
if not toc:
print(f"WARN: khong tim thay TOC trong {build_dir} - bo qua verify (tiep tuc build)")
return 0
print(f"Verify TOC: {toc}")
with open(toc, "r", encoding="utf-8", errors="replace") as f:
raw = f.read()
# TOC Windows co the dung backslash (app\\static) — normalize het ve forward
# slash de kiem tra khong bi false positive. TOC la Python literal nen
# backslash bi DOUBLE-escape ('app\\\\static') — thay 2 lan (\\\\ truoc,
# roi \\) de ca 2 dang deu ve '/'.
text = raw.replace("\\\\", "/").replace("\\", "/")
missing = []
for req in REQUIRED:
# Tim theo prefix thuc su trong TOC (vd 'app/static/js/...' hoac
# string repr 'app/static/...' trong tuple DATA entry).
if not re.search(re.escape(req) + r"(?=[/'\"]|$)", text):
missing.append(req)
if missing:
print(f"ERROR: Bundle thieu: {', '.join(missing)}. Kiem tra engine.spec datas!")
return 1
print("OK: app/static + app/templates co trong bundle.")
return 0
if __name__ == "__main__":
sys.exit(main())
+990
View File
@@ -1,3 +1,109 @@
### [2026-08-08] FIX: verify_bundle.py vẫn false-positive trên Windows — TOC là Python literal nên backslash bị DOUBLE-escape ('app\\\\static')
- **Tóm tắt thay đổi:** Fix LAN 2 normalize `replace('\\','/')` 1 lần KHÔNG đủ: TOC file là Python literal (repr) → path Windows ghi thành `app\\\\static` (2 backslash trên disk) → replace 1 lần ra `app//static` → regex `app/static(?=[/'"])` không match → vẫn báo "Bundle thieu" DÙ bundle đủ. Fix: normalize 2 bước — `replace("\\\\","/")` (bắt double-escape) rồi `replace("\\","/")` (backslash đơn). Verify bằng TOC giả lập Windows repr: cả 2 entry match True.
- **Các file ảnh hưởng:** `tools/verify_bundle.py` (dòng normalize)
- **Ghi chú/Test (nếu có):** Test TOC Windows repr `'app\\\\static\\\\js\\\\app.js'` → normalize ra `app/static/js/app.js` → regex match. User: pull code mới rồi chạy lại build_windows.ps1 (bước 3.5 sẽ hết báo thiếu nếu bundle thực sự đủ).
---
### [2026-08-08] FIX (LAN 3 - GOC ROOT): 'app\\static does not exist' — sys.path thieu CWD khi chay 'pyinstaller' (entry point)
- **Tóm tắt thay đổi:** Lỗi 'app\static does not exist' vẫn tái diễn dù đã 2 lần sửa datas (absolute path roi collect_data_files). Lần này tìm ra GOC ROOT THẬT SỰ: lệnh `pyinstaller engine.spec` (entry-point script cua PyInstaller) KHONG them CWD vao sys.path — chi `python -m PyInstaller` moi them. `collect_data_files('app')` import package 'app' qua sys.path -> khong thay -> tra ve [] AM THAM -> bundle thieu static/templates (dung canh: tren may dev chay `python -m PyInstaller` nen CWD co trong sys.path -> tuong da dung; may Windows chay `pyinstaller` -> CWD khong co -> collect rong -> TOC khong co app/static — dung voi loi verify cua user). Fix trong `engine.spec`:
1. `sys.path.insert(0, _SPEC_ROOT)` TRUOC khi goi collect_data_files — import 'app' luon hoạt dong bat ke CWD.
2. Fallback cuoi: neu collect_data_files van tra ve rong -> datas TINH absolute (app/templates, app/static, app/models) + in WARN de debug.
- **Cac file anh huong:** `engine.spec` (sys.path insert + fallback datas tinh)
- **Ghi chu/Test (neu co):** VERIFY DUNG DIEU KIEN THAT BAI: build tu /tmp (CWD khac project root — giong `pyinstaller` entry point khong co CWD trong sys.path) -> TOC co app/static (124) + app/templates (12), khong co WARN "tra ve rong"; tools/verify_bundle.py -> OK exit 0; chay binary frozen -> /health OK, index 200, static js 200. pytest 86 passed. User: pull engine.spec moi roi chay lai build_windows.ps1.
---
### [2026-08-08] FIX (lan 2): ERR_MODULE_NOT_FOUND '@babel/standalone' vẫn xảy ra — build_windows.ps1 TỰ cài dep nếu thiếu
- **Tóm tắt thay đổi:** User vẫn gặp ERR_MODULE_NOT_FOUND '@babel/standalone' dù đã thêm vào package.json — máy Windows chưa pull package.json mới hoặc npm install chưa cài. Fix: `build_windows.ps1` bước [2/6] — sau `npm install`, TỰ KIỂM TRA `node_modules\@babel\standalone`; thiếu → `npm install @babel/standalone --no-audit --no-fund`; vẫn thiếu → ERROR + exit 1. Không còn phụ thuộc package.json mới trên máy user.
- **Các file ảnh hưởng:** `build_windows.ps1` (bước 2 tự cài @babel/standalone)
- **Ghi chú/Test (nếu có):** File ASCII-only + UTF-8 BOM (không lặp lỗi PS5.1), brace depth 0. User: pull build_windows.ps1 mới (hoặc toàn bộ) rồi chạy lại.
---
### [2026-08-08] FIX: build.mjs ERR_MODULE_NOT_FOUND '@babel/standalone' trên Windows — package.json thiếu dependency
- **Tóm tắt thay đổi:** User chạy build_windows.ps1 (bước 2/6 node build.mjs) nhận `Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@babel/standalone' imported from build.mjs`. Root cause: `build.mjs` import `@babel/standalone` nhưng **package.json KHÔNG khai báo** dependency này (máy dev có sẵn trong node_modules từ trước nên không lộ; máy Windows `npm install` chỉ cài theo package.json → thiếu). Fix: thêm `"@babel/standalone": "^7.29.8"` vào dependencies + chạy `npm install` cập nhật `package-lock.json`.
- **Các file ảnh hưởng:** `package.json` (+@babel/standalone), `package-lock.json` (npm install)
- **Ghi chú/Test (nếu có):** npm install OK (up to date), package-lock có node_modules/@babel/standalone, `node build.mjs` BUILD OK 1123403 bytes + node --check OK. User: pull code mới rồi chạy lại build_windows.ps1.
---
### [2026-08-08] FIX: verify bundle [3.5/6] false-positive 'app/static thieu' + check spec version — tools/verify_bundle.py
- **Tóm tắt thay đổi:** User chạy build_windows.ps1 — bước verify [3.5/6] mới báo "ERROR: Bundle thieu: app/static, app/templates" nhưng thực tế bundle ĐỦ (collect_data_files('app') đã hoạt động — verify bằng build Linux). Root cause: script verify cũ dùng regex `-notmatch "app/static"` trên raw TOC — trên Windows TOC chứa `app\static` (BACKSLASH) → regex forward-slash không match → FALSE POSITIVE báo thiếu. Fix:
1. **tools/verify_bundle.py** (mới): verify TOC bằng Python — đọc text, `replace('\\','/')` normalize backslash→forward, regex theo prefix `app/static(?=[/'"]|$)`; exit 0 OK / 1 thiếu; không có TOC → WARN + exit 0 (không chặn build). Test 4 case: backslash OK, forward OK, thiếu static → ERROR, không TOC → WARN.
2. **build_windows.ps1 [3.5/6]**: trước tiên check `engine.spec` có chứa `collect_data_files('app')` — nếu spec CŨ (chưa pull code mới) → báo rõ "engine.spec CU... Pull code moi" + exit 1 (tránh nhầm lẫn nguyên nhân). Sau đó chạy `python tools\verify_bundle.py` + check `$LASTEXITCODE`.
- **Các file ảnh hưởng:** `tools/verify_bundle.py` (mới), `build_windows.ps1` (bước 3.5 gọi script + check spec)
- **Ghi chú/Test (nếu có):** verify_bundle.py test 4 case đều đúng; chạy với TOC THẬT (build Linux PyInstaller 6.21) → "Thiếu: KHÔNG — OK". Binary frozen: /health OK, index 200, static js 200. pytest 86 passed. User: pull code mới (có tools/verify_bundle.py) rồi chạy lại build_windows.ps1.
---
### [2026-08-08] FIX: build_windows.ps1 lỗi parse PowerShell — "String is missing terminator" (file chứa ký tự Unicode, PS 5.1 đọc theo ANSI)
- **Tóm tắt thay đổi:** User chạy build_windows.ps1 trên Windows nhận ParserError "String is missing terminator" ở dòng 57 + "Missing closing '}'" ở dòng 34. Root cause: bản mình thêm bước verify [3.5/6] có chứa ký tự Unicode (em-dash `—` trong comment + chuỗi tiếng Việt có dấu "thiếu/Dừng/kiểm tra"). PowerShell 5.1 đọc file .ps1 KHÔNG có BOM theo ANSI/Windows-1252 → byte UTF-8 của `—` (E2 80 94) giải mã thành `â€"` — dấu ngoặc kép giả chui vào giữa chuỗi → chuỗi "mất terminator" + block `{}` lệch. Fix:
1. Viết lại toàn bộ build_windows.ps1 **chỉ ASCII** (bỏ dấu tiếng Việt, bỏ em-dash, dùng `->`).
2. Thêm **UTF-8 BOM** (EF BB BF) vào đầu file — PowerShell đọc đúng encoding bất kể.
3. Giữ nguyên bước verify [3.5/6] (TOC check app/static + app/templates).
- **Các file ảnh hưởng:** `build_windows.ps1` (ASCII-only + BOM)
- **Ghi chú/Test (nếu có):** File giờ 0 ký tự non-ASCII, có BOM; brace depth = 0 (cân bằng), không có dòng Write-Host quote lẻ. Lưu ý chung: MỌI .ps1/.bat trong dự án phải ASCII-only + BOM (PS 5.1 ANSI bug) — tránh tiếng Việt có dấu/em-dash trong file script Windows.
---
### [2026-08-08] FIX (LẦN 2): Windows exe vẫn crash 'app\static does not exist' + stuck 'Đang khởi động Engine' — bundle qua collect_data_files + fallback + verify build
- **Tóm tắt thay đổi:** User build bản Windows vẫn gặp RuntimeError `Directory '..._MEIxxxx\app\static' does not exist` (lần 2 — fix trước dùng datas ABSOLUTE theo SPECPATH nhưng trên máy user vẫn thiếu static trong bundle) + UI stuck "Đang khởi động SonicForge Engine". 3 lớp phòng thủ:
1. **engine.spec — bundle qua IMPORT SYSTEM**: bỏ datas đường dẫn tĩnh, thay bằng `collect_data_files('app', excludes=['**/storage/**','**/__pycache__/**','**/*.pyc'])` — PyInstaller tự tìm assets (static/templates/models) qua package import, KHÔNG phụ thuộc CWD/SPECPATH lúc chạy lệnh (nguyên nhân gốc: chạy pyinstaller từ thư mục khác → relative path không resolve → bỏ qua âm thầm). Loại luôn `app/storage` (57MB soundfonts — vô ích trong onefile, config.py đã redirect %APPDATA%). Giữ `md/` (ngoài package) + librosa .pyi.
2. **app/main.py — fallback an toàn**: nếu STATIC_DIR không tồn tại → thử `sys._MEIPASS/app/static``dirname(__file__)/static`; vẫn thiếu → **tự os.makedirs** → StaticFiles không còn crash lúc import (engine không chết, app hiện lỗi rõ thay vì 6 hộp thoại + stuck loader).
3. **build_windows.ps1 — verify post-build [3.5/6]**: sau pyinstaller, đọc Analysis-00.toc kiểm tra `app/static` + `app/templates` có trong bundle — thiếu → in ERROR đỏ + `exit 1` (bắt lỗi NGAY lúc build, không đợi chạy app mới vỡ).
- **Các file ảnh hưởng:** `engine.spec` (collect_data_files('app') + bỏ storage), `app/main.py` (fallback static dir), `build_windows.ps1` (verify TOC post-build)
- **Ghi chú/Test (nếu có):** VERIFY THẬT trên Linux (PyInstaller 6.21, cùng spec): Analysis-00.toc chứa app/static (124), app/templates (12), app/models (5); exe 155MB (giảm 57MB); chạy binary frozen → /health OK, index 200, static js 200, favicon 200, không crash. pytest 86 passed. User cần chạy lại `build_windows.ps1` (bước 3.5 sẽ tự kiểm tra bundle).
---
### [2026-08-08] IMPROVE: About modal — logo dùng favicon của app (app/templates/favicon.svg, serve /favicon.svg)
- **Tóm tắt thay đổi:** User yêu cầu logo trong About modal phải là logo của web/app — favicon lưu trong hệ thống. Trước đây AboutModal dùng div gradient chữ "SF". Fix: thay bằng `<img src="/favicon.svg">` (route đã có sẵn trong app/main.py — serve từ TEMPLATES_DIR; file app/templates/favicon.svg, SVG 1254x1254). Hiển thị 48x48 object-contain, nền tối + border cho nổi trên modal.
- **Các file ảnh hưởng:** `app/static/js/app.jsx` (AboutModal img), `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608081600)
- **Ghi chú/Test (nếu có):** BUILD OK 1123403 bytes, node --check OK, pytest 86 passed. Verify: /favicon.svg 200 image/svg+xml, bundle chứa "favicon.svg".
---
### [2026-08-08] FEAT: Menu Help (About + Hướng dẫn sử dụng) + Tools → Preferences (Theme/Language/Button font size)
- **Tóm tắt thay đổi:** User yêu cầu 2 nhóm tính năng:
1. **Menu Help**:
- `About SonicForge Studio...`**AboutModal** mới: dev **Lộc Phạm**, email **tranloclqd@gmail.com**, version `1.0.0` (khớp tauri.conf.json), build Standalone (Tauri v2 + PyInstaller).
- `Hướng dẫn sử dụng...`**HelpModal** mới: 8 mục hướng dẫn song ngữ (vi/en theo language preference): Bắt đầu nhanh, MIDI & ARM, Piano Roll, FX & Master, SoundFont & VST, AI, Lưu & Xuất, Phím tắt.
2. **Tools → Preferences...****PreferencesModal** mới quản lý:
- **Theme** (5 preset): dark (mặc định), midnight, forest, violet, graphite — áp qua `data-theme` trên `<html>` + CSS variables `--sf-bg/--sf-panel/--sf-header/--sf-border/--sf-accent` (thêm trong index.html `<style>`, override .daw-app-shell/.daw-panel/.daw-header/body/scrollbar).
- **Language**: vi/en — áp ngay cho HelpModal + PreferencesModal (menu chính giữ nguyên — app vốn song ngữ lẫn lộn).
- **Button font size**: sm/md/lg — áp qua `data-btnfont` trên `<html>` + CSS `html[data-btnfont=...] button { font-size: ... !important }`.
3. **Lưu trữ**: localStorage `sf_prefs` + server `/api/v1/user/preferences` (window.SonicAPI.getPreferences/savePreferences — endpoint đã có sẵn). Load server prefs khi khởi động.
- **Các file ảnh hưởng:** `app/static/js/app.jsx` (AboutModal/HelpModal/PreferencesModal mới + state prefs + menu Tools/Help + render 3 modal), `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (CSS theme + data-btnfont + bump v=202608081500)
- **Ghi chú/Test (nếu có):** BUILD OK 1123418 bytes, node --check OK, pytest 86 passed. Verify serve: index 200, precompiled 200, bundle chứa đủ "Hướng dẫn sử dụng", "tranloclqd@gmail.com", "data-btnfont", "Preferences". Lưu ý: button font size dùng !important nên override cả text-xs của Tailwind — đúng ý "đổi cỡ chữ button" nhưng có thể làm một số nút hơi to/nhỏ so với thiết kế gốc.
---
### [2026-08-08] FIX: VU meter giữ animation đúng TRƯỜNG ĐỘ âm thanh khi ARM + nhấn/giữ phím MIDI keyboard
- **Tóm tắt thay đổi:** User yêu cầu: bật ARM + nhấn phím MIDI keyboard preview — khi âm CÒN PLAY thì VU meter phải còn animate cho đúng trường độ âm thanh. Trước đây: note-on → `triggerMidiVuActivity` set peak → VU tick decay 0.75/frame → VU tắt sau ~0.5s DÙ âm còn kêu (note dài 60000ms, chỉ tắt khi note-off). Root cause: VU tick decay theo thời gian, không biết note đang giữ. Fix:
1. **`heldMidiNotesRef`** (mới): đếm số note MIDI đang GIỮ per-track (ARM + keyboard live). Note-on → tăng counter (cả nhánh armed sub-tab PIANO_ROLL lẫn armed main track); note-off → giảm, về 0 thì xóa.
2. **VU tick**: `heldCnt = heldMidiNotesRef.current[vuKey]` — còn note giữ (heldCnt > 0) → GIỮ NGUYÊN peak, KHÔNG decay → VU animate suốt trường độ; hết note (note-off) → decay 0.75 (~0.5s) tắt nhanh như cũ (giữ hành vi user bug 07:15 "hết âm → VU tắt ngay").
3. **stopAllPlayback**: clear luôn `heldMidiNotesRef.current = {}` cùng với `midiVuActivityRef` — tránh sau STOP (âm đã dừng) tick vẫn thấy counter > 0 → giữ peak → VU dính mãi.
- **Các file ảnh hưởng:** `app/static/js/app.jsx` (heldMidiNotesRef + note-on/note-off counters + VU tick giữ peak), `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608081400)
- **Ghi chú/Test (nếu có):** BUILD OK 1109174 bytes, node --check OK, pytest 86 passed + 5 skipped. Luồng: nhấn giữ phím → VU giữ peak; nhả phím (note-off) → VU decay ~0.5s rồi tắt; STOP → VU tắt ngay. Keybed click (mousedown duration 500ms) giữ nguyên hành vi cũ (không qua heldMidiNotesRef).
---
### [2026-08-08] FIX: Chặn browser zoom toàn trang — item/button/label giữ nguyên kích thước khi phóng to UI
- **Tóm tắt thay đổi:** User yêu cầu: khi phóng to giao diện, các item/button/label PHẢI GIỮ NGUYÊN kích thước hiện tại — chỉ các vùng flexible co giãn + cho phép user tự kéo resize (resizer TCP/sidebar/media-explorer/track-height ĐÃ có sẵn). Root cause: layout vốn dùng px cố định (không scale theo window), nhưng **browser zoom toàn trang** (Ctrl+wheel, Ctrl+plus/minus/0, pinch gesture) scale TOÀN BỘ UI → control phóng to theo. WebView2 (Tauri) mặc định bật zoom control. Fix trong `app/templates/index.html`:
1. Meta viewport thêm `maximum-scale=1.0, user-scalable=no`.
2. Inline script chặn zoom ở **capture phase** bằng `preventDefault()` — KHÔNG `stopPropagation()` nên các vùng zoom CHUYÊN DỤNG vẫn nhận event và hoạt động bình thường: timeline zoom (Ctrl+wheel, 18999), piano-roll grid zoom (7351), waveform canvas zoom (12189), EQ canvas wheel (10144).
3. Chặn keydown Ctrl+'+'/'-'/'='/'_'/'0' + gesturestart/gesturechange (pinch Safari/WebKit).
- **Các file ảnh hưởng:** `app/templates/index.html` (meta viewport + zoom-guard script inline)
- **Ghi chú/Test (nếu có):** index.html serve OK (uvicorn), script extract + node --check OK. Không đụng app.jsx/app.precompiled.js (index.html no-cache nên không cần bump stamp). Resizer panel giữ nguyên — user vẫn kéo được TCP width (280-600), sidebar (200-600), media-explorer (20-80%), track height (110-300).
---
### [2026-08-08] FIX: Windows runtime — ModuleNotFoundError scipy.stats._ansari_swilk_statistics + ValueError librosa stub (PyInstaller bundle)
- **Tóm tắt thay đổi:** User chạy bản Windows exe gặp 2 lỗi runtime (hộp thoại error):
1. `ModuleNotFoundError: No module named 'scipy.stats._ansari_swilk_statistics'` — scipy >= 1.18 tách `scipy.stats` thành nhiều module con import LAZY bên trong hàm (vd `_ansari_swilk_statistics`) → hook scipy của PyInstaller không thấy → thiếu trong bundle. Fix: `engine.spec` thêm `collect_submodules('scipy')` vào hiddenimports (collect toàn bộ stats/signal/ndimage/... — không sót module nào).
2. `ValueError: Cannot load imports from non-existent stub '..._MEIxxxx\librosa\__init__.pyi'` — librosa 0.11 dùng `lazy_loader.attach_stub` → lúc RUNTIME cần file `.pyi` tồn tại trên disk, nhưng PyInstaller mặc định KHÔNG bundle `.pyi` (4 file: `__init__.pyi`, `core/`, `feature/`, `util/`). Fix: `datas += collect_data_files('librosa', includes=['**/*.pyi'])`.
- **Các file ảnh hưởng:** `engine.spec` (import collect_data_files + _scipy_hidden + librosa .pyi datas)
- **Ghi chú/Test (nếu có):** VERIFY THẬT bằng build PyInstaller trên Linux (cùng engine.spec, cần objdump/objcopy từ NDK llvm) → chạy binary frozen → `/health` OK, `/` 200, `/static/js/app.precompiled.js` 200, không còn 2 lỗi trên. pytest 86 passed. Lưu ý: máy user cần build lại bằng `build_windows.ps1` để có exe mới.
---
### [2026-08-08] FIX: Windows build crash — 'app\static does not exist' (PyInstaller datas relative) + FIX: ARM track + MIDI keyboard → VU meter đứng yên
- **Tóm tắt thay đổi:** 2 bug trên bản Windows standalone:
1. **Build crash 6 hộp thoại error**: `RuntimeError: Directory '...\_MEIxxxx\app\static' does not exist` khi chạy daw_engine.exe. Root cause: `app/main.py` dùng `os.path.dirname(__file__)` để mount /static — khi frozen `__file__` trỏ vào thư mục giải nén tạm `_MEI...` KHÔNG phải `sys._MEIPASS`; đồng thời `engine.spec` datas dùng path RELATIVE (resolve theo CWD lúc chạy `pyinstaller`, chạy từ nơi khác → bỏ qua âm thầm → static/templates/models thiếu trong bundle). Fix: `main.py` dùng `settings.APP_DIR` (freeze-aware, config.py đã có); `engine.spec` đổi toàn bộ datas + pathex sang ABSOLUTE từ `SPECPATH` + thêm `app/models` (project_schema.json — projects.py dùng, trước đây thiếu khi frozen).
2. **ARM track + bấm phím MIDI keyboard → VU meter không animation**: vòng tick VU (`masterVUAnimRef` useEffect) có block early-return `if (!isPlaying && !isRecActive)` vẽ VU RỖNG và return — nên bật ARM + bấm phím MIDI (không play/record) thì `midiVuActivityRef` có giá trị nhưng không bao giờ được đọc → VU đứng yên. Fix: thêm điều kiện `liveMidiVu` (scan midiVuActivityRef > 0.001) — có MIDI activity live thì KHÔNG early-return, track VU nhảy theo activity + decay như bình thường.
- **Các file ảnh hưởng:** `app/main.py` (STATIC_DIR → settings.APP_DIR), `engine.spec` (datas/pathex absolute + app/models), `app/static/js/app.jsx` (VU tick liveMidiVu guard), `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608081200)
- **Ghi chú/Test (nếu có):** BUILD OK 1107797 bytes, node --check OK, pytest 86 passed + 5 skipped (test_sf_convert fail pre-existing — path `/app/...` docker-only). Backend import OK — STATIC_DIR resolve đúng cả dev lẫn frozen.
---
### [2026-08-04] Task: Mở PIANO ROLL TAB lúc main đang play KHÔNG được tắt âm — handleEditMidiInTab stopAllPlayback có điều kiện
- **Tóm tắt thay đổi:** User báo "đang play main session, dblclick mở Piano roll tab → âm bị tắt". Thủ phạm: `handleEditMidiInTab` (15983) gọi `stopAllPlayback()` VÔ ĐIỀU KIỆN ngay khi mở tab — dừng mọi nguồn main đang phát. Fix: `if (!isPlaying) stopAllPlayback()` — chỉ clean-stop khi KHÔNG play main (tránh stuck route khi đổi tab giữa sub-tab); main đang play → mở tab → âm main tiếp tục. Nhánh play sub-tab tự stopAllPlayback trước khi play nên không xung đột route.
- **Các file ảnh hưởng:** `app/static/js/app.jsx` (handleEditMidiInTab), `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608042300)
@@ -2041,3 +2147,887 @@
- **FIX (app.jsx):** nút Đóng chuyển từ cuối toolbar → SAU nút MIDI ghost notes (hàng 1) + `ml-auto` (đẩy phải); nhóm Lưu/Export giữ cuối hàng 2 + `ml-auto`; Export → **Export MIDI**. Emoji 🎵 Chuyển giọng bị patch tool double-escape → sửa bằng emoji thật (verify: 0 double-escape còn lại).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061030), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → hàng 1 phải có [Đóng]; hàng 2 phải: [Lưu] [Export MIDI] ở cuối bên phải.
### [2026-08-06 11:00] Task: Ẩn gợi ý đăng nhập sau lần đăng nhập đầu (username/password hints + nút Điền nhanh)
- **Yêu cầu user:** sau lần đăng nhập hệ thống đầu tiên → xóa gợi ý: "(Tùy chọn - Admin có thể bỏ trống)", "(Lần đầu: admin123)", nút "🔑 Điền Nhanh Tài Khoản Admin".
- **FIX:**
(1) Backend `auth.py`: endpoint **`GET /api/v1/auth/first-time`** → `{first_time: bool}` — true khi admin vẫn dùng mật khẩu mặc định (verify_password admin123 + must_change_password); sau khi đổi → false.
(2) Frontend AuthModal: state `firstTime` + fetch khi modal mở; 3 gợi ý render có điều kiện `firstTime &&` — hết lần đầu → ẩn vĩnh viễn.
- **Các file ảnh hưởng:** `app/api/v1/auth.py`, `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061100), `wiki.md`. Rebuild precompiled + restart backend.
- **Ghi chú/Test:** restart backend → login admin (mật khẩu còn mặc định) → vẫn thấy gợi ý; đổi mật khẩu → logout → login lại → KHÔNG còn gợi ý/nút Điền nhanh.
### [2026-08-06 11:30] Task: Gợi ý đăng nhập vẫn hiện sau khi đổi mật khẩu — fail-closed (ẩn khi endpoint lỗi)
- **Báo cáo user:** đã đổi mật khẩu nhưng gợi ý vẫn xuất hiện.
- **Nguyên nhân nghi:** backend chưa restart → `/api/v1/auth/first-time` 404 → catch cũ `setFirstTime(true)` → gợi ý hiện (fail-open sai hướng).
- **FIX (app.jsx AuthModal):** catch → `setFirstTime(false)` + else branch → fail-closed: endpoint lỗi/404 → **ẨN gợi ý**. Endpoint OK: `verify_password("admin123", hash mới)` = false → first_time=false → ẩn (đã đúng logic backend).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061130), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → login → gợi ý ẩn (kể cả khi backend chưa restart). Khuyến nghị restart backend để endpoint hoạt động đúng.
### [2026-08-06 12:00] Task: New project — reset toàn bộ (đóng tab, clear selection, reset instrument context)
- **Yêu cầu user:** tạo new project → đóng hết tab đang mở, clear selection, reset instrument đã load về mặc định.
- **FIX (app.jsx — 2 nơi: menu File → New Project + Ctrl+N):**
- `stopAllPlayback()` (dừng mọi play)
- `setSubTabs([])` + `setSessionTabs([])` + `setActiveTab('main')` (đóng hết tab)
- `setSelectedItemIds(new Set())` (clear multi-selection)
- Reset context instrument: `trackMidiChannelsRef.current = {}` + clear `trackMidiBypassMap`/`trackAudioBypassMap`/`trackMasteringBypassMap` (delete từng key — const map)
- Reset project: 2 track mặc định, selectedTrackId '1', projectName '', currentProjectId null, xóa localStorage id/name
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061200), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → mở vài tab + chọn track → File → New Project (hoặc Ctrl+N) → tab đóng hết, selection trống, instrument track về mặc định.
### [2026-08-06 12:30] Task: SECTION-TAB 5 bug — Ctrl+S, strip controls, undo, section play, mute isolation
- **Báo cáo user (SECTION-TAB):** (1) Ctrl+S phải lưu section; (2) volume slider + nút strip không tác dụng; (3) không undo/redo cut/paste/delete/move; (4) MAIN play section item câm sau khi lưu; (5) mute ở section mixer mute luôn track MAIN.
- **FIX:**
(1) **Ctrl+S**: handler đầu (15915) save project vô điều kiện + return → block thứ 2 (16007) chết → sửa handler đầu tab-aware: `session_``handleSaveSectionTab` + toast; khác → save project.
(2+5) **Handlers setTracks (MAIN) → updateActiveTracks (context-aware)**: `toggleTrackMute`, `toggleTrackSoloEvaluate`, `updateTrackVolumeDb`, `updateTrackPan`, `updateTrackProp` — section strip giờ tác dụng lên track SECTION (không còn mute track MAIN).
(3) **Undo section**: chuỗi đã context-aware (captureTrackSnapshot/captureAllTracksSnapshot → activeTracks; restore applyTrackState → updateActiveTracks) — sau fix (2) các entry mute/volume đúng context; item ops (delete/move/paste 20417/20603/20764+) đã push đúng.
(4) **Section play**: code path đã verify (18847-18957: schedule clip+midi sub-track → subNode → parent → route → mastering; sec.tracks lưu sau save 16381-16395) — nếu vẫn câm cần log console (`[SonicSF]` noteon + sfId).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061230), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section tab: Ctrl+S lưu section; kéo volume/mute tác dụng trong section; Ctrl+Z sau cut/paste; main play section item có âm; mute section không ảnh hưởng main.
### [2026-08-06 13:00] Task: SECTION-TAB volume/mute — áp gain cho sub-node (key _sub_) realtime
- **Báo cáo user:** volume mixer strip không tác dụng âm thanh; mute section phải CHỈ ảnh hưởng section.
- **Nguyên nhân:** node play section là **sub-node** (key `<parent>_sub_<trackId>` — 18697) — gain chỉ set lúc TẠO node (18703) — `updateTrackVolumeDb`/`applyAllTrackMuteSolo` lookup `activeTrackNodesRef[trackId]` (key thường) → KHÔNG thấy sub-node → volume/mute đổi STATE nhưng âm section không đổi.
- **FIX (app.jsx):**
(1) `updateTrackVolumeDb`: sau khi set gain node thường → **scan `_sub_<trackId>`** → set gain sub-node realtime.
(2) `applyAllTrackMuteSolo`: sau `setTrackNodeGain(node, ...)`**scan `_sub_<t.id>`** → set gain sub-node theo audibleGain (mute/solo section áp node play section — activeTracksRef = section tracks → KHÔNG đụng track MAIN).
(3) (Đã fix vòng trước) handlers setTracks → updateActiveTracks — mute/volume chỉ đổi tracks SECTION.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061300), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section tab: kéo volume nghe đổi ngay; mute section track → câm trong section, main không bị; unmute lại.
### [2026-08-06 13:30] Task: Mixer fader dùng updateTrackProp — volume section vẫn không đổi âm
- **Báo cáo user:** volume slider SECTION-TAB mixer KHÔNG đổi âm lượng.
- **Nguyên nhân:** mixer strip (TrackStripConsole — 27165) truyền `onUpdateTrack: updateTrackProp` — fader (1560) gọi `onUpdateTrack(tid, {volumeDb})` — fix vòng 13:00 đặt scan sub-node trong `updateTrackVolumeDb` (TCP slider) — **mixer dùng updateTrackProp — SAI CHỖ → không áp gain node.**
- **FIX (app.jsx):** `updateTrackProp` — khi props có `volumeDb` → áp gain realtime node thường + scan `_sub_<trackId>` (sub-node section) — mixer section + mixer main đều đổi âm ngay.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061330), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section tab mixer: kéo volume → âm section đổi ngay (cả khi main đang play section item); main mixer cũng đổi ngay.
### [2026-08-06 14:00] Task: SECTION-TAB — volume MIDI track qua CC7 (channel volume) + fix collision channel
- **Báo cáo user:** AI tạo track trong SECTION dùng chung channel track trước → volume track sau vô tác dụng cho đến khi track trước MUTE.
- **Nguyên nhân:** CC7 (channel volume) chỉ set 100/0 (mute/unmute) — VOLUME track KHÔNG áp qua channel → track MIDI dùng chung channel: CC7 cuối thắng → volume 1 track không tác dụng.
- **FIX (app.jsx — 2 chỗ):**
(1) `applyAllTrackMuteSolo` (14135): CC7 = **volume track** (audible ? volLin*100 clamp 1-127 : 0) — không còn 100/0 cứng.
(2) Sync effect (14719): cùng logic CC7-volume — volume slider → trackMuteSoloSig (gồm volumeDb) → effect → CC7 mới.
(3) (Đã có) AI Var clone `midiChannel: undefined` (23288) + `ensureTrackMidiChannel` tự cấp channel riêng khi trùng (30: `!inUse.has(track.midiChannel)`) — collision được xử lý khi assign.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061400), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: tạo AI Var mới → kéo volume từng track → âm đổi theo track; mute track trước → track sau vẫn đúng volume. Nếu track AI Var CŨ (tạo trước fix midiChannel) vẫn dùng chung channel → xóa + tạo lại.
### [2026-08-06 14:30] Task: Nút A (audio bypass) trên section strip không auto-áp — sub-node thiếu route
- **Báo cáo user:** nút A xám (bypass on) nhưng audio không bypass — phải bấm 2 lần mới có tác dụng.
- **Nguyên nhân:** sub-node section (key `<parent>_sub_<subId>`) chỉ có gainNode → parent — **KHÔNG có route (routeGain/dryGain)**`__setTrackBypass` scan node theo `indexOf(trackId + '_sub_')` (sai key — sub-node key bắt đầu bằng PARENT id) → nút A không áp được; node play section không có gì để toggle.
- **FIX (app.jsx):**
(1) `getOrCreateSubTrackNode`: thêm **route riêng** cho sub-track — `routeGain` (mastering path → parent) + `dryGain` (→ masterBus.dryInput) — gain khởi tạo theo `effAudioBypass(subTrack)` — nút A áp ngay từ lúc tạo node.
(2) `__setTrackBypass` (audio): scan `k === trackId || k.endsWith('_sub_' + trackId)` (đúng key sub-node của track đó) → `setMasteringRoute` realtime.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061430), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: nút A track audioclip → xám = bypass ngay (nghe hết mastering); sáng = qua mastering.
### [2026-08-06 15:00] Task: Nút A section — route tạo node dùng effAudioBypass bị override mastering-ON → bypass mất khi stop+play
- **Báo cáo user:** A mặc định tắt → play không bypass; bấm A ON rồi OFF mới bypass; stop + play lại → NOT BYPASS.
- **Nguyên nhân:** sub-node route tạo bằng `effAudioBypass(subTrack)` — hàm này override `false` khi mastering ON (yêu cầu v03:00) → route luôn no-bypass lúc TẠO node → phải bấm A 2 lần (__setTrackBypass set route trực tiếp); stop → node bị xóa/tái tạo → route lại ép false → mất bypass.
- **FIX (app.jsx getOrCreateSubTrackNode):** dùng **map THẬT** `trackAudioBypassMap[subTrack.id]` (trạng thái nút A) — không qua effAudioBypass → route tạo theo nút A; stop+play → node tái tạo → route đúng trạng thái nút A.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061500), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: bật A (xám) → play → bypass ngay; stop + play lại → VẪN bypass; tắt A (sáng) → qua mastering.
### [2026-08-06 15:30] Task: SECTION-TAB audioclip PHẢI bypass mặc định — addNewTrack context-aware (A xám)
- **Yêu cầu user:** nút A mặc định tắt nhưng audioclip item PHẢI bypass khi play; stop + play lại không được mất bypass.
- **FIX (app.jsx addNewTrack 21440):** `audioBypass: isSectionCtx ? true : false` — activeTab `session_` → track mới trong SECTION mặc định **BYPASS** (nút A xám, audio raw); MAIN giữ false (qua mastering).
- **Kết hợp vòng trước:** sub-node route dùng map THẬT (không override mastering-ON) → route đúng nút A; stop+play → node tái tạo → route đúng trạng thái.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061530), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: tạo track mới + drop audioclip → play → BYPASS ngay (A xám); stop + play lại → VẪN bypass. Track CŨ (tạo trước fix, audioBypass false) → bấm A 1 lần (xám) → từ đó bypass đúng.
### [2026-08-06 16:00] Task: SECTION-TAB bypass rules — nút A (audio) + nút ♪ (midi) áp đúng cho sub-node
- **Yêu cầu user:** nút A tắt → bypass Audioclip; A bật → không bypass; nút ♪ tắt → midi bypass; ♪ bật → midi không bypass.
- **FIX (app.jsx):**
(1) `getOrCreateSubTrackNode`: subBypass theo LOẠI item của sub-track — có CLIPS → `trackAudioBypassMap` (nút A); có MIDI → `trackMidiBypassMap` (nút ♪) — route tạo đúng trạng thái nút.
(2) `__setTrackBypass` nhánh 'midi': **scan sub-node** (`endsWith('_sub_'+trackId)`) → `setMasteringRoute` — nút ♪ trên section strip toggle route sub-node MIDI ngay.
- **Kết hợp:** sub-node route (routeGain→parent / dryGain→masterBus.dryInput) — bypass → dry (raw); không bypass → qua mastering.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061600), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: track audioclip — A tắt → bypass, A bật → qua mastering; track MIDI — ♪ tắt → bypass, ♪ bật → qua mastering. Stop+play lại → trạng thái giữ nguyên.
### [2026-08-06 16:30] Task: ReferenceError handleOpenProject — hàm chỉ tồn tại trong ProfileModal, App render reference sai scope
- **Báo cáo user:** bundle 061600 không mở/reload lại dự án — console: `Uncaught ReferenceError: handleOpenProject is not defined`.
- **Nguyên nhân:** `handleOpenProject` (5624) nằm BÊN TRONG component ProfileModal — nhưng OpenProjectModal (File → Open Project) nhận `onOpenCloud` closure ở **App render** (27727: `handleOpenProject(projId, projName)`) — reference ngoài scope → ReferenceError khi bấm Mở.
- **FIX (app.jsx):** thêm **App-level `handleOpenProject`** (trước render) — logic: getCloudProject → deserializeProjectFromSchema → setTracks/mastering/sessionTabs/subTabs + localStorage — giống ProfileModal. ProfileModal giữ bản riêng (tự scope).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061630), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → File → Open Project → bấm MỞ → dự án nạp + reload lại đúng.
### [2026-08-06 17:00] Task: Restore project stack overflow — guard cycle trong deserializeTracksList (SECTION_ITEM tự trỏ)
- **Báo cáo user:** reload page → `restoreLastSessionProject failed: RangeError: Maximum call stack size exceeded`.
- **Nguyên nhân:** `deserializeTracksList` (9023) → SECTION_ITEM (9059) → gọi lại `deserializeTracksList(secContainer.tracks,...)` — section item trỏ về section ĐANG deserialize (hoặc chu trình A→B→A) → recursion VÔ HẠN → stack overflow.
- **FIX (app.jsx deserializeTracksList):**
(1) **Visited-set**: truyền `visitedSections` xuống; SECTION_ITEM trỏ về section có trong set (cycle) → `tracks: null` (không đệ quy tiếp).
(2) **Depth guard**: `curDepth > 12 → return []` — chặn lồng sâu bất thường.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061700), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → reload page → dự án restore bình thường (section tự trỏ → deserialize an toàn, tracks null thay vì crash).
### [2026-08-06 17:30] Task: GỠ override mastering-ON — nút A/♪ quyết định bypass (rules SECTION-TAB)
- **Báo cáo user:** rules bypass vẫn chưa áp được — log: `[Bypass] node created track 1 initial audioBypass= true routeGain= 1 dryGain= 0` — bypass TRUE nhưng routeGain=1 (INVERTED).
- **Nguyên nhân:** `createMasteringRoute` dòng 142 `if (masteringChainOn()) bypass = false;` + `effAudioBypass/effMidiBypass` override mastering-ON (yêu cầu v03:00) → khi mastering ON, route LUÔN ép qua chain → nút A/♪ bị nuốt.
- **FIX (app.jsx):** GỠ override ở 3 chỗ: `createMasteringRoute` + `effMidiBypass` + `effAudioBypass`**NÚT A/♪ quyết định bypass, không phụ thuộc mastering ON** (rules hiện tại của user thắng yêu cầu cũ v03:00).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061730), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → SECTION-TAB: A tắt (xám) → routeGain=0/dryGain=1 (bypass) ngay lần play đầu; A bật → qua mastering. Log `[Bypass] node created ... audioBypass= true routeGain= 0 dryGain= 1` ✓.
### [2026-08-06 18:00] Task: Copy/Paste notes giữa các PIANO ROLL TAB (clipboard)
- **Yêu cầu user:** copy midi note từ PIANO ROLL TAB này sang tab khác.
- **FIX (app.jsx):**
(1) App: state `pianoRollClipboard` + prop `onCopyNotes`/`clipboardNotes` cho PianoRollTabEditor (deep-copy khi copy).
(2) Editor toolbar (hàng 2): nút **📋 Copy** (toàn bộ notes → clipboard + toast) + **📥 Paste** (notes clipboard → append cuối tab hiện tại, sinh id MỚI `note_cp_*`, `pushToUndo` → Ctrl+Z hoàn tác).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061800), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → tab A: Copy → mở tab B → Paste → notes xuất hiện; Ctrl+Z hoàn tác. Verify: 0 double-escape emoji.
### [2026-08-06 18:30] Task: NotSupportedError createBuffer 0 frames — guard Math.max(1) cho mọi createBuffer
- **Báo cáo user:** `Uncaught NotSupportedError: Failed to execute 'createBuffer' ... frames (0) less than or equal to minimum bound (0)`.
- **Nguyên nhân:** các op audio (sub-tab cut/copy/paste, normalize/fade/gain) tính `len`/`data.length` = 0 (vùng chọn rỗng hoặc buffer 0 frame) → `createBuffer(..., 0, ...)` → throw.
- **FIX (app.jsx — guard `Math.max(1, ...)`):** sub-tab Cut (15637/15644), Copy (15672), Paste (15727), clonedBuffer ×3 (15554/15577/15601). Chỗ 11802 đã guard sẵn.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061830), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → thao tác cut/copy audio vùng chọn rỗng → hết crash.
### [2026-08-06 18:45] Task: Section block MAIN không vẽ inner items preview (items chỉ thuộc SECTION-TAB)
- **Báo cáo user:** open project → các item của SECTION-TAB xuất hiện như clip/midi trên MAIN SESSION (ngoài block) — muốn sửa.
- **Chẩn đoán:** file .sfs SẠCH (main track chỉ SECTION_ITEM; section_store chứa nội dung ✓) — KHÔNG leak dữ liệu. Thủ phạm = **RENDER**: main timeline vẽ **preview inner items** bên trong section block (2492-2557 — sub-tracks + waveform + note bars) → trông như "items tải vào MAIN".
- **FIX (app.jsx render):** GỠ preview sub-tracks — section block trên MAIN chỉ hiện **tên + màu + bounds**. Dữ liệu `sec.tracks` VẪN giữ (play section item + SECTION-TAB không ảnh hưởng).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061845), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → open Section_test → MAIN chỉ thấy block section (tên + màu), không còn item bên trong; mở SECTION-TAB → nội dung đầy đủ; play section item → vẫn có âm.
### [2026-08-06 19:00] Task: Audioclip trong section mất sau save+open — loadAudioBuffersForTracks không nạp clip bên trong section
- **Báo cáo user:** project MAIN chỉ có 1 section item (trong section: 1 midi + 1 audioclip). Sau khi lưu (ghi đè) + mở lại → audioclip bị mất, chỉ còn khung "(audio chưa được tải)".
- **Nguyên nhân:** `loadAudioBuffersForTracks` (14841) chỉ nạp `t.buffer` + `t.clips` (cấp MAIN track) — **KHÔNG đệ quy vào section item's `tracks`** → audioclip bên trong section không bao giờ được fetch/decode → buffer null → "(audio chưa được tải)".
- **FIX (app.jsx):** thêm vòng lặp section — `t.sections[].tracks[].clips``tryLoad(serverFileId)` mỗi clip → buffer; trả về `sections: updatedSections` trong cả 2 nhánh return.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061900), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → save project → mở lại → audioclip trong section hiện đầy đủ (hết "(audio chưa được tải)"); play section → có âm cả midi + audio.
### [2026-08-06 19:30] Task: Lưu zoom vào project + cho xóa item lỗi/rỗng (khung viền)
- **Yêu cầu user:** (1) lưu zoom khi save + khôi phục khi mở; (2) cho phép xóa item lỗi (khung viền "audio chưa được tải"/item rỗng/dữ liệu lỗi).
- **FIX (app.jsx):**
(1) **Zoom**: App effect `window.__currentZoom = zoom` → serialize metadata thêm `zoom` → deserialize trả `zoom` → restore + open (2 chỗ) `setZoom(result.zoom)`.
(2) **Guard xóa track** (2 chỗ 17124/20131): item LỖI/rỗng không chặn — `hasClips = clips.some(c => c.buffer)` (clip không buffer = khung viền → không tính); `hasMidi = midiItems.some(m => m.notes?.length)`; `hasSections = sections.some(s => s.tracks?.some(st => st.clips?.some(c => c.buffer) || st.midiItems?.some(m => m.notes?.length)))` — track chỉ chứa item lỗi → xóa được.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061930), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → zoom vào 2x → save → mở lại → zoom giữ 2x. Xóa track chỉ chứa item "(audio chưa được tải)" → xóa được.
### [2026-08-06 19:45] Task: Khôi phục preview items trong section block (sau mở project)
- **Báo cáo user:** sau khi mở lại dự án, section item không vẽ lại các item chứa trong nó.
- **FIX (app.jsx render 2491):** khôi phục preview sub-tracks trong section block (waveform bars + midi note bars) — bị gỡ ở 18:45. Kết hợp fix 19:00 (loadAudioBuffersForTracks đệ quy nạp buffers section) → preview hiện đầy đủ sau open.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061945), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → open project → section block hiện waveform + note bars của items bên trong.
### [2026-08-06 20:00] Task: Audioclip chèn trong SECTION-TAB không lưu — race upload + track buffer-only bị lọc
- **Báo cáo user:** audioclip chèn trong SECTION-TAB không được lưu.
- **Nguyên nhân:** `handleSaveSectionTab` lọc `contentTracks = tracks.filter(tr => tr.clips?.length > 0 || ...)` — track vừa chèn audio (upload async chưa tạo clip — hoặc buffer-only) bị LỌC → audioclip không vào section item → save mất.
- **FIX (app.jsx handleSaveSectionTab):**
(1) Filter thêm `|| !!tr.buffer` — gồm track buffer-only.
(2) Buffer-only → tạo **default clip** (`default_<id>` — buffer, startTime, name) → serialize bắt được AUDIO_ITEM.
(3) Serialize đã fallback `c.serverFileId || t.serverFileId` (8959) ✓ — file id lấy từ track nếu clip chưa có.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062000), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: chèn audioclip → lưu section → save project → mở lại → audioclip còn đầy đủ (có âm).
### [2026-08-06 20:30] Task: Watchdog SECTION-TAB — chỉ rebuild khi NaN (hết playhead reset khi play)
- **Báo cáo user:** audioclip mất → di chuyển playhead đến vị trí đó nhấn play → playhead về đầu track + không play.
- **Nguyên nhân:** watchdog (18168) — section tab không phải piano roll → nhánh `pk < 0.001` — audioclip mất/rest tự nhiên → im lặng 750ms → recovery `stopAllPlayback` (playhead reset 0) + restart → "playhead về đầu + không play".
- **FIX (app.jsx):** thêm `isSectionTab` (`activeTab` bắt đầu `session_`) → nhánh `(isPianoRoll || isSectionTab ? nanOut : ...)` — section tab chỉ rebuild khi output NaN (chain chết), im lặng bình thường KHÔNG recovery → playhead giữ vị trí.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062030), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section tab: play tại vị trí audioclip đã mất → playhead giữ nguyên (không về đầu); play bình thường vẫn hoạt động.
### [2026-08-06 21:00] Task: Section item mở nhấp đôi không chứa audioclip — clone xóa clips
- **Báo cáo user:** mở lại dự án → nhấp đôi section item → chỉ là section item, không chứa các items như trước khi lưu.
- **Nguyên nhân:** `handleEditSectionInTab` (16515) clone `JSON.parse(JSON.stringify(section.tracks))` với **`clips: []`** (reset cố định) → audioclip bị xóa khỏi section editor; JSON clone cũng DROP AudioBuffer.
- **FIX (app.jsx):** clone giữ **clips + buffer THẬT** (`srcClips = (t.clips||[]).map(c => ({...c, buffer: c.buffer||null}))`) — JSON chỉ dùng cho field metadata; buffer gắn lại từ source live.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062100), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → open project → nhấp đôi section item → section editor đầy đủ clips (audioclip có waveform) + midi; play nghe được.
### [2026-08-06 21:30] Task: Reload mất audioclip section — clip thiếu serverFileId → upload buffer-only tại thời điểm lưu
- **Báo cáo user:** reload → mất audioclip đã lưu trong SECTION-TAB.
- **Chẩn đoán (DB):** save mới nhất — `AUDIO_ITEM: server_file_id: None | url: ''` — clip KHÔNG có file reference (upload sớm thất bại/race — uploadToServer trả null "client-side only") → reload → load fail → audioclip mất.
- **FIX (app.jsx):**
(1) Thêm `encodeWavBlob` (WAV encoder tối thiểu — PCM 16-bit).
(2) `handleSaveSectionTab` → async + `ensureClipsUploaded(contentTracks)` — clip chưa serverFileId + có buffer → encode WAV → uploadToServer → gán `c.serverFileId` TRƯỚC khi ghi vào section item → serialized AUDIO_ITEM có file_id → reload nạp được.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062130), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: chèn audioclip → Lưu section (chờ upload — console không có "upload failed") → save project → reload → audioclip còn + có âm.
### [2026-08-06 22:00] Task: Section editor (nhấp đôi) trống sau reload — session tab không nhận buffers (merge nested)
- **Báo cáo user:** section item MAIN vẽ đúng content sau reload, nhưng CHỈNH SỬA (nhấp đôi) → chỉ là block, không phải nội dung đã lưu.
- **Nguyên nhân:** loadAudioBuffersForTracks merge session tabs (14995): `updatedTracks.find(u => u.id === t.id)` — find ở TOP-LEVEL — track của session tab có id TRÙNG track bên TRONG section item (nested) → không tìm thấy → session tab giữ tracks deserialize (clips không buffer) → editor trống/không waveform.
- **FIX (app.jsx):** build `allLoadedTracks` PHẲNG (updatedTracks + section items' inner tracks) → merge session tabs tìm trong map phẳng → session tab nhận tracks có buffers.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062200), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → save + reload → nhấp đôi section item → section editor đầy đủ (audioclip waveform + midi); play nghe được.
### [2026-08-06 22:30] Task: Section item TỰ TRỎ (block lồng) + mất MIDI track — insert thiếu sectionId + fallback s.id
- **Báo cáo user:** SECTION-TAB hiện block lồng (section item) + MIDI item chỉ là hình + play không lên.
- **Chẩn đoán (DB):** section track 01 chứa `SECTION_ITEM: sec_1786020136359 → sec_1786020136359` (TỰ TRỎ chính section); track MIDI (play_VST 292 notes) biến mất khỏi section (console tracks=2).
- **Nguyên nhân:** `insertSectionAtPlayhead` (21822) tạo section item thiếu `sectionId` → serialize (9008) `referenced_section_id: s.sectionId || s.id` fallback → **chính id item = id section → tự trỏ** → deserialize block lồng + sectionStore ghi đè → mất track MIDI.
- **FIX (app.jsx):**
(1) `handleSaveSectionTab` — LỌC section item tự-trỏ khỏi contentTracks (`(s.sectionId||s.id) !== tab.sectionId`).
(2) Serialize (9008) — BỎ fallback `s.id` — chỉ `s.sectionId` (thiếu → block rỗng, không tự-trỏ).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062230), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → project CŨ (đã dính tự-trỏ): xóa block lồng trong section tab + thêm lại MIDI track → lưu → save → reload → section tab đầy đủ (audioclip + midi), play được.
### [2026-08-06 23:00] Task: DIAG — section tab vẫn hiện block sau reload (data sạch + bundle mới)
- **Trạng thái:** DB save sạch (section: 2 tracks midi+audio, không SECTION_ITEM); production đã deploy 2230 (referenced_section_id:s.section + allLoadedTracks ✓) — NHƯNG user vẫn thấy block trong SECTION-TAB + midi mất + play block không âm.
- **Hành động:** thêm diagnostic log sau restore — `[Restore] sessionTab <id> sectionId <sid> tracks N sections N midi N clips N` — để xác định block sinh từ đâu (restored sessionTabs có sections không; hay sinh lúc nhấp đôi).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062300), `wiki.md`. Rebuild precompiled.
- **Chờ user:** build + deploy 2300 → reload → dán log `[Restore] sessionTab` + log lúc nhấp đôi (có `[Play]` + `[SonicSF] noteon` không).
### [2026-08-06 23:30] Task: FIX ID COLLISION merge session tabs — block lồng + mất MIDI (bug từ fix 2200)
- **Báo cáo user:** mất midi item + vẫn hiển thị section block trong SECTION-TAB.
- **Nguyên nhân:** fix 2200 (merge session tabs) dùng `allLoadedTracks` (main tracks + section inner tracks) — `find(u => u.id === t.id)` lấy track ĐẦU TIÊN — **inner track id 1 trùng MAIN track id 1** → session tab Track 01 bị THAY bằng MAIN track 01 (chứa SECTION_ITEM + không midi) → **block lồng + mất MIDI + play block không âm**.
- **FIX (app.jsx):** merge session tabs CHỈ match với `sectionInnerTracks` (inner tracks của section items) — không match track main top-level.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062330), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → deploy → reload → nhấp đôi section item → SECTION-TAB đầy đủ (midi + audioclip đúng vị trí) + play cả 2.
### [2026-08-06 23:45] Task: Media Explorer (F6) — close panel phải tắt preview âm thanh
- **Báo cáo user:** đóng Media Explorer panel → preview âm thanh vẫn phát.
- **Nguyên nhân:** panel KHÔNG unmount khi đóng (ẩn bằng `display:none` 27465) — cleanup unmount không chạy → preview source + SonicSF notes tiếp tục.
- **FIX (app.jsx):** truyền `active={showMediaExplorer}` vào MediaExplorerPanel + effect `if (active === false) stopMediaPlayback()` — đóng F6 → dừng source + loop + SonicSF.stopAll.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062345 — sửa typo `</script></script>` kép), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → F6 → preview 1 file → F6 close → âm dừng ngay.
### [2026-08-07 00:00] Task: VU meter SECTION-TAB không animation khi play
- **Báo cáo user:** play ở MAIN → chuyển SECTION-TAB check item đang play → VU meter không animation realtime.
- **Nguyên nhân (2 lỗi):** (1) VU tick (24603) lookup `trackNodes[trackId]` — key THƯỜNG — node của section tab là SUB-NODE (key `<parent>_sub_<trackId>`) → không khớp → node undefined; (2) sub-node KHÔNG có `analyserNode` (chỉ `{gainNode, pannerNode, route}`) → VU không có gì để đọc.
- **FIX (app.jsx):** (1) VU tick — fallback scan `_sub_<trackId>` keys → vNode; đọc analyser từ `vNode.analyserNode`; (2) getOrCreateSubTrackNode — thêm `analyserNode` (gainNode → analyser → routeGain/dryGain) → VU đọc được tín hiệu section.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070000), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → play MAIN → chuyển SECTION-TAB → VU meters nhảy realtime theo track đang play.
### [2026-08-07 00:30] Task: MIDI section bị bypass mastering (route qua parent) + VU section không đúng nhịp
- **Báo cáo user:** play MAIN → nhấp đôi vào section item → (1) VU meter animation không đúng nhịp âm thanh; (2) âm thanh MIDI item bị bypass qua mastering chain.
- **Nguyên nhân:**
(1) **MIDI bypass**: sub-node routeGain nối vào PARENT NODE → qua route của parent track (parent `audio_bypass: true` → routeGain 0/dryGain 1) → MIDI/audio section bị kéo vào DRY bus dù sub-track ♪/A tắt → "bị bypass mastering".
(2) **VU không đúng nhịp**: scheduling MIDI của section item KHÔNG trigger `triggerMidiVuActivity` (main-track midi có ở 19066) → VU chỉ dựa analyser (trễ/smoothing) → không nhảy theo từng note.
- **FIX (app.jsx):**
(1) getOrCreateSubTrackNode — mastering path `routeGain.connect(window.masterBus.input)` TRỰC TIẾP (không qua parent — parent route có bypass riêng); fallback parentNode khi không có masterBus.
(2) Section midi scheduling (19182) — thêm `triggerMidiVuActivity(subTrack.id, note.velocity)` sau mỗi playNote.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070030), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → play MAIN → nhấp đôi section → MIDI qua mastering (nghe EQ/limiter tác dụng) + VU nhảy đúng theo note.
### [2026-08-07 01:00] Task: MIDI section tab bị bypass — createMasteringRoute dùng audioBypass cho MIDI-only track
- **Báo cáo user (kèm log):** nhấp đôi section → `setOutputDestination to: track node (sfEntry)` + noteon chạy nhưng MIDI bị bypass mastering.
- **Nguyên nhân:** SECTION-TAB play dùng PLAIN node (getOrCreateTrackNode) → `createMasteringRoute` đọc `trackAudioBypassMap`/`track.audioBypass` — section track default `audio_bypass: true` (round 48) → route dry → **MIDI kéo vào dry bus dù ♪ tắt**. (sfBypass/SF route đã đúng ♪ ở 1041/18667 — chỉ AUDIO route sai.)
- **FIX (app.jsx createMasteringRoute):** bypass theo LOẠI nội dung — track **MIDI-only** (midiItems, không clips/buffer) → dùng `trackMidiBypassMap`/`midiBypass` (♪); track có clips → `audioBypass` (A) như cũ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070100), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → nhấp đôi section → MIDI qua mastering (EQ/limiter nghe rõ); log `[Bypass]` routeGain=1/dryGain=0 cho midi track.
### [2026-08-07 01:30] Task: First play sau double-click bị bypass (node STALE) + VU track chứa section item
- **Báo cáo user:** (1) nhấp đôi section → play lần ĐẦU midi bị bypass; stop + play lại → đúng. (2) play section item trên track → VU track đó không animation.
- **Nguyên nhân:**
(1) **Node STALE**: section tab track id TRÙNG main track id — node tạo từ MAIN play (route theo data main — audioBypass=true → dry) được cache lại → first play trong section tab dùng node cũ → bypass; stop (wipe nodes) → play lại → node mới đúng.
(2) **VU track main**: âm section đi thẳng masterBus.input (fix 00:30) → KHÔNG qua node chính → analyser node chính không có tín hiệu → VU đứng yên.
- **FIX (app.jsx):**
(1) Helper `routeBypassFor(track)` (bypass theo content-type — MIDI-only → ♪, có clips → A) — áp: createMasteringRoute (đã có), startTrackPlayback re-apply route mỗi lần schedule (hết stale), mastering-settings effect (14906 thay effAudioBypass).
(2) VU tick — scan sub-nodes (`k.indexOf(trackId + '_sub_') === 0`) khi node chính không có tín hiệu → peak sub-node analyser → VU track main animation theo section.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070130), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → nhấp đôi section → play lần ĐẦU → midi qua mastering ngay; play section trên MAIN → VU track đó nhảy.
### [2026-08-07 02:00] Task: VU MAIN không nhảy cho midi section item — triggerMidiVuActivity thiếu parent
- **Báo cáo user:** (1) first play sau double-click midi vẫn bị bypass (lặp — cần xác nhận bundle 0130); (2) quay về MAIN — section item play midi notes → VU meter không animation.
- **Nguyên nhân (2):** midiVuActivity keyed theo `subTrack.id` (section) — VU MAIN đọc `midiVuActivityRef[mainTrackId]` → miss → VU main đứng yên.
- **FIX (app.jsx 19212):** section midi scheduling trigger CẢ 2 — `triggerMidiVuActivity(subTrack.id, ...)` + `triggerMidiVuActivity(track.id, ...)` (parent/main track) → VU main nhảy theo section item đang play.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070200), `wiki.md`. Rebuild precompiled.
- **Chờ xác nhận bug (1):** user build + test 0130/0200 — nếu first play VẪN bypass → dán console: `[Bypass] node created` (routeGain/dryGain) + `setOutputDestination` lúc nhấp đôi + play lần đầu.
### [2026-08-07 02:30] Task: Re-apply route sub-node khi section item play (main) — hết stale bypass
- **Yêu cầu user:** (1) BẮT BUỘC VU meter hiển thị trên track đang play items (MAIN + SECTION-TAB); (2) chuyển tab MAIN → SECTION-TAB midi item đang play KHÔNG được bypass.
- **FIX (app.jsx 19132):** section item play (main) — sau getOrCreateSubTrackNode — re-apply route sub-node theo `routeBypassFor(subTrack)` (MIDI-only → ♪, clips → A) mỗi lần schedule → hết stale route (tương tự re-apply plain node ở 0130).
- **VU stack hiện tại (các fix trước):** sub-node có analyserNode (00:00) → VU section tab đọc sub-node (00:00); trigger midi VU cho section (00:30); VU main scan sub-nodes khi node chính câm (01:30); trigger parent track cho VU main (02:00). **BẮT BUỘC hiển thị = đã phủ: main track (subPeak + midi activity) + section tab (vNode sub-node + midi activity).**
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070230), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → (1) play section ở MAIN + trong SECTION-TAB → VU nhảy đúng track; (2) MAIN → double-click SECTION-TAB → play lần đầu → MIDI qua mastering (không bypass).
### [2026-08-07 03:30] Task: MIDI section bị bypass khi vào SECTION-TAB — masteringBypass fallback + clone kế thừa flag
- **Báo cáo user:** đang play MAIN SESSION → double-click section item vào SECTION-TAB → âm MIDI item bị bypass (không qua mastering chain).
- **Nguyên nhân (data thật — project "Section test" trong DB):** sub-track section (id TRÙNG main track id 1, 2) lưu `mastering_bypass=True` + `audio_bypass=True` (kế thừa từ track gốc). Mọi hàm quyết định bypass (`effMidiBypass`/`effAudioBypass`/`createMasteringRoute`/`routeBypassFor`/`getOrCreateSubTrackNode`) fallback `?? track.masteringBypass` → khi map chưa sync/thiếu entry (timing khi double-click + play lần đầu) → track MIDI-only bị tính bypass=TRUE → SF/sub-node route vào DRY bus (bypass mastering) dù ♪ tắt. Ngoài ra `handleEditSectionInTab` clone section tracks GIỮ NGUYÊN audioBypass/masteringBypass của track gốc → sync effect (14757) ghi map audio sai cho track section.
- **FIX (app.jsx):**
(1) BỎ fallback `?? masteringBypass` khỏi mọi hàm quyết định route (effMidiBypass/effAudioBypass/createMasteringRoute/routeBypassFor/getOrCreateSubTrackNode) — nút A/♪ + `audioBypass`/`midiBypass` là nguồn duy nhất; `masteringBypass` không còn quyết định route (field legacy kế thừa sai).
(2) `handleEditSectionInTab` (16595) — clone section tracks ép reset bypass theo context: `masteringBypass:false, audioBypass:true (section raw default), midiBypass:false` — hết kế thừa flag track gốc.
(3) Sync effect (14757): `trackMasteringBypassMap[t.id] = !!t.masteringBypass` (trước gán nhầm `!!t.audioBypass`).
(4) `updateSfRouting` (18813): `effMidiBypass(t.id)``effMidiBypass(t)` — truyền string → map lookup sai → sfRouteGain bị ép 1 (♪ vô hiệu qua đường này).
(5) Log chẩn đoán: `[Bypass] sfEntry created track <id> midiBypass=...` (lúc tạo node) + `[Bypass] sf route sync track <id> ...` (updateSfRouting ép route) — test còn lỗi thì dán log.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070330), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → play MAIN → double-click section vào SECTION-TAB → MIDI qua mastering (nghe EQ/limiter); play trong SECTION-TAB → MIDI vẫn qua mastering (♪ tắt); bật ♪ → bypass đúng ý. Nếu còn bypass → dán console `[Bypass]` logs.
### [2026-08-07 03:35] Task: MIDI section vẫn bypass sau 0330 — thêm log chẩn đoán [SFRoute]/[Bypass] reapply
- **Báo cáo user (log 0330):** play MAIN → double-click section → `[SonicSF] setOutputDestination to: track node (sfEntry)` + noteons chạy → bấm Play tab (isPlaying=true) → stopAllPlayback → SF về masterBus.input. Log THIẾU các dòng `[Bypass]`/`[Play] offset` của bản 0330 → chưa xác định được route thực tế (sfRouteGain/routeGain giá trị bao nhiêu lúc nghe bypass).
- **Log mới thêm (app.jsx):**
(1) `[SFRoute] activeTab=... midiAudible=...` — updateSfRouting: track nào audible + đích tính.
(2) `[SFRoute] dest=sfEntry node <id> sfRouteGain=... sfDryGain=...` — đích SF + giá trị route hiện tại TRƯỚC khi ép.
(3) `[SFRoute] dest=gainNode node <id> (no sfEntry) route.routeGain=... dryGain=...` — đích fallback gainNode.
(4) `[Bypass] reapply track <id> contentType=midi/audio bypass=... → routeGain=.../dryGain=...` — startTrackPlayback re-apply route.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070335), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → double-click section vào SECTION-TAB → nghe bypass → **dán TOÀN BỘ console (không filter)** — cần các dòng `[SFRoute]`, `[Bypass]`, `[Play] offset`, `setOutputDestination` để chốt đường bypass chính xác.
### [2026-08-07 03:40] Task: MIDI section bypass — ID TRÙNG main track → node/map collision (CHỐT NGUYÊN NHÂN)
- **Log 0335 (đã chốt):** play MAIN → `[SFRoute] activeTab=main midiAudible=` (rỗng → SF về masterBus.input — MIDI qua mastering ✓). Double-click → `[SFRoute] activeTab=session_... midiAudible=1(midi1)``[SFRoute] dest=gainNode node 1 (no sfEntry) — route.routeGain=0 route.dryGain=1` → SF route vào NODE '1' → **route dry → MIDI section BỊ BYPASS**. (Lưu ý: log soundfontPlayer "track node (sfEntry)" là log SAI — luôn in "sfEntry" cho mọi node khác null; đích thực là gainNode.)
- **Nguyên nhân gốc:** section clone giữ id sub-track TRÙNG main track id (data: main 1/2 = sub 1/2). (1) `updateSfRouting` lookup node theo id → trúng node MAIN (route theo ♪ MAIN = bật = dry) → SF (MIDI section) kéo vào dry dù ♪ section tắt. (2) Bypass map keyed theo id bị GHI ĐÈ CHÉO (clone audioBypass=true ghi đè map main; ♪ main ảnh hưởng route section).
- **FIX (app.jsx):**
(1) MỌI hàm quyết định route (`effMidiBypass`/`effAudioBypass`/`createMasteringRoute`/`routeBypassFor`/sub-node): **ƯU TIÊN field track object (`track.midiBypass`/`track.audioBypass`)** — object phân biệt đúng context; map chỉ fallback (realtime nút bấm, track không field).
(2) `updateSfRouting`: activeTab là `session_*` → lookup node ƯU TIÊN SUB-NODE (key `<parent>_sub_<id>` — route theo ♪ section + thẳng masterBus.input); không có sub-node → null (SF về masterBus.input — qua mastering, KHÔNG bypass). KHÔNG bao giờ route vào node MAIN khi ở section tab.
(3) Sync effect (14754): đảo thứ tự — session/clone TRƯỚC, MAIN SAU (main thắng khi id trùng — map fallback đúng context; clone dùng field).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070340), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → double-click section vào SECTION-TAB → MIDI qua mastering (nghe EQ/limiter); log kỳ vọng: `[SFRoute] dest=gainNode node 1 ...` → KHÔNG còn (sub-node hoặc masterBus.input); `route.routeGain=1`. Play section tab → MIDI qua mastering (♪ tắt); bật ♪ section → bypass đúng ý.
### [2026-08-07 03:45] Task: MIDI bypass khi mở MIDI item SECTION-TAB → PIANO ROLL TAB
- **Báo cáo user:** sau fix 0340 (section tab OK) — mở MIDI item từ SECTION-TAB vào PIANO ROLL TAB → MIDI bị bypass.
- **Nguyên nhân:** cùng cơ chế collision id — PIANO ROLL của track section có `parent_tab_id = session_*`, `trackId` = id sub-track TRÙNG main track id. `updateSfRouting` nhánh PIANO_ROLL lookup `activeTrackNodesRef.current[trackId]` → trúng NODE MAIN (route theo ♪ main — dry) → SF (notes piano roll) kéo vào dry dù ♪ section tắt. Xảy ra ngay khi ĐỔI TAB (effect 14814 → updateSfRouting) — không cần play.
- **FIX (app.jsx updateSfRouting nhánh PIANO_ROLL):** khi `_activeSub.parent_tab_id` startsWith `session_` → lookup node ƯU TIÊN SUB-NODE (key `endsWith('_sub_' + trackId)` — route theo ♪ section + thẳng masterBus.input); không có sub-node → null (SF về masterBus.input — qua mastering, KHÔNG bypass). Không bao giờ route vào node MAIN cho piano roll của section.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070345), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → double-click section (SECTION-TAB) → double-click MIDI item → PIANO ROLL tab → MIDI qua mastering (nghe EQ/limiter); log `[SFRoute]` không còn dest node MAIN (route 0/1) cho piano roll section.
### [2026-08-07 03:50] Task: Playhead vẫn di chuyển trong PIANO ROLL TAB khi mở MIDI item từ SECTION-TAB
- **Báo cáo user:** mở MIDI item từ SECTION-TAB (đang play) → PIANO ROLL TAB → playhead vẫn tiếp tục di chuyển — cần dừng.
- **Phân tích:** piano roll view vẽ playhead theo `st.currentTime` (chỉ cập nhật khi tab play) — khi mở tab (chưa play) code cũ `return` ở nhánh sub-tab KHÔNG setCurrentTime → playhead đứng yên — NHƯNG return đó GIẾT CHẾT rAF loop: main/section play chạy ngầm (handleEditMidiInTab 16427 không dừng âm) → watchdog master-silence chết, âm main không dừng đúng projectEnd, và khi quay lại tab playhead NHẢY VỌT theo `elapsed` thật (trông như vẫn di chuyển/sai lệch).
- **FIX (app.jsx updatePlayhead nhánh sub-tab):** tách 2 trường hợp — (1) sub-tab KHÔNG play: KHÔNG setCurrentTime (playhead đứng yên) NHƯNG nếu `isPlaying` (main/session chạy ngầm) → vẫn check projectEnd (stop đúng lúc) + TIẾP TỤC requestAnimationFrame (loop sống, watchdog sống, quay lại tab không nhảy); (2) sub-tab play: giữ nguyên hành vi cũ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070350), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → double-click section (SECTION-TAB) → double-click MIDI item → PIANO ROLL TAB → playhead KHÔNG di chuyển (đứng yên); âm main tiếp tục + dừng đúng cuối bài; quay lại SECTION-TAB → playhead đúng vị trí âm (không nhảy vọt). Nếu playhead vẫn chạy khi CHƯA bấm play → xác nhận lại: có bấm Space/Play trong piano roll không (bấm play → playhead chạy là đúng hành vi).
### [2026-08-07 03:55] Task: FIX hồi quy 0350 — CÂM âm khi mở PIANO ROLL từ SECTION-TAB (projectEnd=1s ở sub-tab)
- **Báo cáo user (log 0350):** mở MIDI item từ SECTION-TAB → PIANO ROLL → `[SFRoute] activeTab=midi_... midiAudible=1(midi1)``setOutputDestination to: masterBus.input` → KHÔNG noteon tiếp theo → CÂM (bản 0345 play tiếp).
- **Nguyên nhân (hồi quy fix 0350):** nhánh updatePlayhead sub-tab không-play check `updatedTime >= projectEndRef.current` — nhưng `projectEnd` tính theo activeTab: piano roll tab (sub-tab) KHÔNG main/không session → `tab=undefined → max=0`**projectEnd = 1s** → updatedTime (elapsed THẬT từ lúc play main — đã >1s) >= 1 → `stopAllPlayback()` NGAY → panic notes + xóa nodes (activeTrackNodesRef={}) → SF về masterBus.input + KHÔNG noteon = CÂM.
- **FIX (app.jsx):** bỏ check projectEnd khỏi nhánh sub-tab không-play — chỉ `requestAnimationFrame` tiếp tục (loop sống cho playhead không nhảy khi quay lại). Âm main đã schedule hết từ startTrackPlayback — tự hết đúng lúc; quay lại MAIN → nhánh main tự stop tại projectEnd THẬT.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070355), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → double-click section (SECTION-TAB) → double-click MIDI item → PIANO ROLL TAB → âm section TIẾP TỤC play (không câm) + playhead piano roll ĐỨNG YÊN; quay lại SECTION-TAB → playhead đúng vị trí âm.
### [2026-08-07 04:00] Task: Quay lại MAIN/SECTION sau khi Space trong PIANO ROLL → CÂM — resume main play
- **Báo cáo user (log 0355):** mở MIDI item từ SECTION-TAB → PIANO ROLL (playhead đứng yên ✓ — fix 0350/0355); bấm Space → playhead piano roll chạy; **quay lại MAIN/SECTION → CÂM** (log: `[SFRoute] activeTab=main midiAudible=` + `setOutputDestination to: masterBus.input` — SF về masterBus nhưng KHÔNG noteon — main play đã chết).
- **Nguyên nhân:** Space trong PIANO ROLL → `handlePlayPause` nhánh sub-tab → `stopAllPlayback()` DỪNG main/section play (xóa nodes + panic) → chơi piano roll. Quay lại MAIN/SECTION → không còn gì play → câm.
- **FIX (app.jsx):** resume main/section play khi rời piano roll tab đã play:
(1) `mainResumeRef` mới — lưu `{offset: startOffsetTimeRef.current, audioTime: ctx.currentTime}` lúc MỞ piano roll tab khi main đang play (handleEditMidiInTab — nhánh isPlaying=true).
(2) Effect đổi tab (14814): rời PIANO ROLL tab đã play về MAIN hoặc `session_*` + có `mainResumeRef``stopAllPlayback()` + `startTrackPlayback(offset + elapsed thật)` + `setIsPlaying(true)` — âm main/section tiếp tục từ ĐÚNG vị trí (không chơi lại từ đầu, không câm). Các trường hợp khác: clear `mainResumeRef` (không resume trùng).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070400), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → double-click section (SECTION-TAB) → double-click MIDI item → PIANO ROLL (playhead đứng yên) → Space (playhead piano roll chạy) → quay lại MAIN/SECTION → ÂM TIẾP TỤC từ đúng vị trí (log `[Resume] main play từ X.XX s`); KHÔNG bấm Space → quay lại → âm vẫn tiếp tục (không resume trùng).
### [2026-08-07 04:05] Task: FIX 0400 — resume không chạy (mainResumeRef bị clear nhầm bởi chính effect đổi tab)
- **Báo cáo user (log 0400):** piano roll play (Space) → `dest=sfEntry node 1 ... MASTERING` + noteons ✓ → chuyển về SECTION-TAB → `setOutputDestination to: masterBus.input` (node null) → KHÔNG có log `[Resume]` → CÂM.
- **Nguyên nhân:** handleEditMidiInTab set `mainResumeRef` (mở tab lúc main play) → setActiveTab → effect đổi tab (14814) chạy với `prevTab='session_...'``prevSub = subTabsRef.find('session_...')` = **undefined** → rơi vào nhánh `else` cũ → **`mainResumeRef.current = null` — XÓA SẠCH resume point NGAY khi VÀO piano roll** → quay lại section: `_resume=null` → không resume → câm.
- **FIX (app.jsx effect 14814):** nhánh clear đổi thành `else if (prevSub && prevSub.type === 'PIANO_ROLL')` — chỉ clear khi THỰC SỰ rời khỏi piano roll tab (không play — main ngầm tiếp tục tự nhiên). prevTab = main/session (đang VÀO piano roll) → KHÔNG clear — resume point giữ nguyên.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070405), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → double-click section → double-click MIDI item → PIANO ROLL (playhead đứng yên) → Space (piano roll play) → quay lại SECTION-TAB → **log `[Resume] main play từ X.XX s` + ÂM TIẾP TỤC từ đúng vị trí** (SF route: `dest=sfEntry node 1 ... MASTERING`); KHÔNG Space → quay lại → âm ngầm tiếp tục (không resume trùng).
### [2026-08-07 04:10] Task: Playhead PIANO ROLL không di chuyển khi đang play (di chuyển tab qua lại)
- **Báo cáo user (log 0405):** piano roll play hoạt động (schedule 248 notes + noteons + `dest=sfEntry node 1 ... MASTERING` ✓) — nhưng sau khi di chuyển tab qua lại → ở PIANO ROLL TAB playhead không di chuyển khi đang play.
- **Phân tích:** khi rời piano roll tab → effect đổi tab stop sub-tab play (isPlaying:false) + resume main/section (âm chạy). Quay LẠI piano roll tab → tab KHÔNG play → nhánh updatePlayhead sub-tab không-play KHÔNG set st.currentTime → playhead piano roll ĐỨNG YÊN — dù âm MAIN/SECTION (resume) đang phát → user thấy "đang play mà playhead không chạy".
- **FIX (app.jsx updatePlayhead nhánh sub-tab không-play):** nếu `isPlaying` (main/section đang play — refs startOffsetTime/startAudioTime là của main) → setSubTabs `currentTime = startOffsetTimeRef + (ctx.currentTime - startAudioTimeRef)` — playhead piano roll ĐỒNG BỘ theo vị trí âm thật (chạy theo main play). Không có play nào → đứng yên (chế độ edit). Loop luôn tiếp tục rAF.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070410), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → di chuyển tab qua lại (SECTION-TAB ↔ PIANO ROLL) → ở PIANO ROLL TAB playhead chạy theo âm đang phát (đồng bộ vị trí thật); không play gì → playhead đứng yên; bấm Space trong piano roll → playhead chạy theo tab.
### [2026-08-07 04:15] Task: PIANO ROLL — Space không capture play/stop + chuyển tab về MAIN/SECTION bị CÂM
- **Báo cáo user (log 0410):** (1) trong PIANO ROLL TAB bấm Space không play/stop; (2) piano roll play → nhấn play/stop transport → chuyển sang MAIN/SECTION-TAB → CÂM (log: chuyển session → `setOutputDestination to: masterBus.input` + hết noteon — vì effect đổi tab stopAllPlayback dừng piano roll + không có gì resume).
- **Nguyên nhân:**
(1) Keydown handler (15938) bypass MỌI hotkey khi focus INPUT/TEXTAREA/contentEditable — piano roll tab có input focus (snap/rename...) → Space chết.
(2) Effect đổi tab (14814) — rời sub-tab đang play → `stopAllPlayback()` (dừng piano roll — luật cũ "rời sub-tab play → stop") → về MAIN/SECTION không còn âm → CÂM.
- **FIX (app.jsx):**
(1) Keydown handler: activeTab là PIANO ROLL (`midi_*`) → Space LUÔN play/stop (kể cả focus input); phím khác vẫn bypass.
(2) Effect đổi tab: rời PIANO ROLL tab đang play → KHÔNG stopAllPlayback (SF notes đã schedule — tiếp tục kêu tự nhiên — không câm); audio sub-tab (buffer source riêng) giữ stop như cũ. Resume block (mainResumeRef) giữ nguyên — tự stop + resume main nếu có.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070415), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → PIANO ROLL TAB → Space play/stop hoạt động (kể cả khi input focus); piano roll play → chuyển sang MAIN/SECTION-TAB → âm piano roll TIẾP TỤC (hết notes tự nhiên — không câm); quay lại tab → playhead đồng bộ.
### [2026-08-07 04:20] Task: Playhead NGỪNG khi chuyển tab PIANO ROLL (play thuần) → MAIN/SECTION — không đồng bộ âm
- **Báo cáo user:** preview piano roll (play thuần — main chưa play) → chuyển tab sang MAIN/SECTION-TAB → playhead ngừng di chuyển (dù âm piano roll còn kêu — fix 0415 không stop âm).
- **Nguyên nhân:** rời piano roll play → setSubTabs isPlaying:false + KHÔNG stop âm (0415) — nhưng `isPlaying` (main) vẫn false → updatePlayhead nhánh MAIN `if (!isPlaying) return` → playhead đứng yên — KHÔNG đồng bộ với âm piano roll đang phát.
- **FIX (app.jsx effect đổi tab 14814):** rời PIANO ROLL play → về MAIN/`session_*` + mainResumeRef null (play thuần) → `setIsPlaying(true)` — nhánh main updatePlayhead chạy → playhead main/section di chuyển theo elapsed (refs đang là của piano roll play — đồng bộ với âm). Hết bài → nhánh main tự stop. Resume block (mainResumeRef) giữ nguyên ưu tiên trước.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070420), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → PIANO ROLL TAB → Space play (main chưa play) → chuyển MAIN/SECTION-TAB → playhead TIẾP TỤC di chuyển (log `[Playhead] tiếp tục theo piano roll`), âm piano roll vẫn kêu hết notes; quay lại piano roll → playhead đồng bộ.
### [2026-08-07 04:25] Task: Space không play/stop sau khi chuyển tab — focus tab header button nuốt space
- **Báo cáo user:** chuyển tab → focus tự động nằm trên tên tab (tab header button) → nhấn Space thường không có tác dụng — phải click vào tab active thì Space mới capture được.
- **Nguyên nhân:** tab header là `<button>` — sau khi chuyển tab (click header / mở tab bằng double-click item), focus giữ trên button tab header (có thể là header của tab KHÁC — chưa active). Space: keydown → handler play/stop ✓ — NHƯNG browser fire CLICK TỔNG HỢP ở keyup space trên button → click tab header → CHUYỂN TAB — nhìn như Space không play/stop (vừa play vừa đổi tab).
- **FIX (app.jsx keydown handler — block Space):** trước khi play/stop → `document.activeElement` nếu là `BUTTON``blur()` — keyup sau đó target body → KHÔNG click tab header → Space play/stop ổn định ở mọi tab.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070425), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → chuyển tab bằng click header (hoặc mở piano roll bằng double-click) → KHÔNG click thêm → nhấn Space → play/stop đúng tab hiện tại (không chuyển tab, không mất tác dụng).
### [2026-08-07 04:30] Task: PIANO ROLL — Space 2 lần mới stop khi main/section đang play ngầm
- **Báo cáo user:** MAIN/SECTION đang play (MIDI kêu) → chuyển sang PIANO ROLL TAB → Space 1 lần KHÔNG stop (vẫn nghe âm) — Space lần 2 mới stop — "không phát hiện MIDI đang play".
- **Nguyên nhân:** handlePlayPause nhánh sub-tab: `st.isPlaying` (piano roll tab) = false (chưa play tab) → rơi vào else → **stopAllPlayback (dừng main) + PLAY PIANO ROLL ngay** → âm vẫn còn (piano roll) → user tưởng "không stop". Space lần 2: st.isPlaying=true → stop ✓.
- **FIX (app.jsx handlePlayPause nhánh sub-tab):** đầu nhánh — nếu `isPlaying` (main/section đang play ngầm) → `stopAllPlayback()` + setSubTabs isPlaying:false (mọi tab) + `setIsPlaying(false)` → return — Space = STOP trước, không play sub-tab. Space lần sau (đã stop) → play sub-tab bình thường.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070430), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → mở PIANO ROLL tab (main play ngầm) → Space 1 lần → DỪNG hẳn (không còn âm); Space lần nữa → play piano roll (toggle bình thường).
### [2026-08-07 04:35] Task: ĐỒNG BỘ playhead 2 chiều SECTION-TAB ↔ PIANO ROLL (vị trí item)
- **Yêu cầu user:** (1) play MIDI item trên SECTION-TAB ở vị trí X → sang PIANO ROLL → play tiếp tại vị trí tương ứng (X - item.startTime); (2) play PIANO ROLL tại Y → quay lại SECTION-TAB → play tại item.startTime + Y; (3) SECTION play vượt ngoài duration item → PIANO ROLL bắt đầu từ đầu (0).
- **FIX (app.jsx):**
(1) Helper `getPianoRollItemInfo(st)` — tìm item (trackId + target_id) trong context đúng (parent session tab → sessionTabsRef tracks; main → activeTracksRef) → `{startTime, duration}`. Dùng REFS — an toàn closure cũ (rAF loop).
(2) updatePlayhead nhánh sub-tab không-play: `st.currentTime = playPos - item.startTime` (clamp: < startTime hoặc ≥ startTime+duration → 0 — vượt duration → đầu).
(3) Effect đổi tab — rời PIANO ROLL play về MAIN/SECTION: resumeAt = `item.startTime + st.currentTime` (ưu tiên); về MAIN mà item thuộc section → cộng thêm secStart (vị trí section trên main); fallback mainResumeRef; fallback cuối setIsPlaying(true) (playhead chạy theo âm còn kêu).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070435), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → SECTION-TAB play (playhead trong item) → sang PIANO ROLL → playhead đúng vị trí trong item; play piano roll → về SECTION-TAB → play đúng item.startTime + vị trí piano roll (log `[Resume] play tại X.XX s (item.startTime=... + st.currentTime=...)`); SECTION play vượt quá item → piano roll về 0.
### [2026-08-07 04:40] Task: Xác nhận + log chẩn đoán — Đồng bộ 2 chiều MAIN SESSION ↔ PIANO ROLL
- **Yêu cầu user:** đồng bộ 2 chiều MAIN SESSION ↔ PIANO ROLL (tương tự SECTION-TAB ↔ PIANO ROLL 0435): main play ở vị trí X → mở piano roll → playhead trong item đúng (X item.startTime); piano roll play tại Y → về MAIN → play tại item.startTime + Y; main play vượt duration item → piano roll về 0.
- **Trạng thái:** fix 0435 ĐÃ generic — `getPianoRollItemInfo` (parent_tab_id null → `activeTracksRef` = MAIN tracks — item.startTime main global) + 2 nhánh (vào tab: local = ph item.startTime; rời tab: resumeAt = item.startTime + st.currentTime) tự áp dụng cho MAIN. Không cần sửa logic — thêm LOG chẩn đoán để xác nhận.
- **FIX (app.jsx):** nhánh updatePlayhead sub-tab không-play — log `[Sync] PR <id> ph=<vị trí play> item.start=<...> dur=<...> → local=<...>` (rate-limited ~2 lần/giây) — xác nhận giá trị đồng bộ realtime.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070440), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN (playhead trong MIDI item) → mở piano roll → log `[Sync]` cho thấy local tăng theo (ph item.start); play piano roll → về MAIN → log `[Resume] play tại item.startTime + st.currentTime`; main play vượt item → local = 0. Nếu giá trị sai → dán log.
### [2026-08-07 04:45] Task: MAIN/SECTION hết midi note → PIANO ROLL playhead chưa về 0
- **Báo cáo user:** main/section play hết (không còn midi note) → trong PIANO ROLL TAB playhead vẫn giữ vị trí cũ — chưa về 0.
- **Nguyên nhân:** khi play DỪNG (isPlaying → false — hết project/stop) → nhánh updatePlayhead sub-tab không-play `if (isPlaying)` FALSE → KHÔNG setSubTabs → playhead piano roll giữ local cuối (giữa item) — không về 0.
- **FIX (app.jsx):** effect `[isPlaying]` mới — detect transition playing→dừng (prevIsPlayingRef): nếu đang ở PIANO ROLL tab (`midi_*`) + tab KHÔNG play → `setSubTabs currentTime = 0` — playhead về đầu. Tab đang play (piano roll play riêng) → giữ nguyên (không đụng).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070445), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → mở PIANO ROLL (playhead chạy trong item) → đợi play hết (projectEnd) → playhead piano roll TỰ VỀ 0; bấm stop (space) → cũng về 0; piano roll đang play riêng → không bị về 0.
### [2026-08-07 04:50] Task: (1) PIANO ROLL zoom không persist phiên (2) Mở project lưu → timeline zoom out thu nhỏ góc
- **Báo cáo user:** (1) PIANO ROLL TAB không giữ kích thước zoom ở phiên làm việc trước; (2) MAIN SESSION — mở dự án đã lưu → timeline bị zoom out thu nhỏ ở góc — zoom in mới về kích thước đã lưu.
- **Nguyên nhân:**
(1) `rollZoom` (PianoRollTabEditor 6926) là local state khởi tạo 60 — mỗi lần mở tab → reset — không persist.
(2) Serialize project (9267) lưu `window.__currentZoom || 1.0` — nhưng `__currentZoom` do MEDIA EXPLORER set (11583) — không phải zoom timeline (App 13910) → project LUÔN lưu 1.0 → load `setZoom(1.0)` → timeline thu nhỏ góc trái.
- **FIX (app.jsx):**
(1) `rollZoom` khởi tạo từ `localStorage['sf_pr_zoom']` (clamp 15-250) + effect lưu khi đổi.
(2) App zoom effect set `window.__sfTimelineZoom = zoom`; serialize dùng `__sfTimelineZoom || __currentZoom || 1.0`; load (15211 + 24987): `result.zoom > 1 && setZoom` — bỏ qua 1.0 (data cũ lỗi) → giữ zoom hiện tại/localStorage.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070450), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** (1) PIANO ROLL zoom in/out → đóng tab → mở lại (hoặc reload) → zoom giữ nguyên; (2) LƯU project (zoom hiện tại) → mở lại → zoom ĐÚNG kích thước đã lưu (không thu nhỏ); project CŨ (lưu 1.0) → mở → giữ zoom hiện tại (không bị thu nhỏ).
### [2026-08-07 04:55] Task: Zoom reset khi chuyển tab (trong phiên) — thêm log chẩn đoán
- **Báo cáo user:** trong phiên làm việc, chuyển tab → kích thước zoom không lưu lại — bị reset về mặc định.
- **Trạng thái:** zoom MAIN timeline là App state global (không reset khi chuyển tab); piano roll đã persist localStorage (0450). Nghi vấn: effect minZoom (18009) ép `setZoom(minZoom)` khi vào tab có maxDuration nhỏ (section ngắn) — làm mất zoom đã set. Chưa tìm thấy nơi reset về mặc định cố định.
- **FIX (app.jsx):** thêm log chẩn đoán — (1) `[Zoom] đổi tab <prev> → <new> | zoom=<...> | minZoom=<...>` (effect activeTab); (2) `[Zoom] ép lên minZoom=... (zoom=...) tab=... maxDuration=...` (effect minZoom — chỉ khi ép).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070455), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → zoom MAIN (hoặc piano roll) → chuyển tab qua lại → dán log `[Zoom]` — xác định tab nào + giá trị zoom/minZoom khi reset.
### [2026-08-07 05:00] Task: CHỐT — Zoom bị ép khi chuyển tab (log [Zoom] xác nhận)
- **Log user (0455):** vào PIANO ROLL tab → `[Zoom] đổi tab ... → midi_... | zoom=32.78 | minZoom=37.54` + `[Zoom] ép lên minZoom=37.54 (zoom=32.78) tab=midi_... maxDuration=33.88` → về main/section zoom GIỮ 37.54 (không khôi phục 32.78/24.63) = "zoom reset khi chuyển tab".
- **Nguyên nhân:** `maxDuration` tính theo activeTab — PIANO ROLL tab (sub-tab) KHÔNG main/không session → `tab=undefined → max=0` → max = 12 bars buffer (33.88s) → `minZoom = viewportWidth/maxDuration` LỚN (37.54) → effect minZoom (18009) ép `setZoom(minZoom)` → zoom bị đẩy lên → quay lại main không khôi phục (state giữ giá trị bị ép).
- **FIX (app.jsx effect minZoom):** chỉ ép minZoom khi activeTab là `main` hoặc `session_*` — sub-tab (PIANO ROLL/audio) KHÔNG ép (timeline không hiển thị — ép vô nghĩa + hại). Zoom giữ nguyên khi vào/ra sub-tab.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070500), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → zoom MAIN (vd 24.6) → vào SECTION-TAB → vào PIANO ROLL → KHÔNG còn log `[Zoom] ép lên minZoom` → quay lại MAIN → zoom GIỮ NGUYÊN (không reset).
### [2026-08-07 05:05] Task: Section item — giữ kích thước đã resize (không tự đổi khi lưu SECTION-TAB) + MAIN play theo duration item
- **Yêu cầu user:** (1) resize section item trên MAIN → giữ nguyên kích thước — KHÔNG tự động thay đổi khi lưu SECTION-TAB; (2) MAIN SESSION chỉ play duration của section item — mặc kệ SECTION-TAB duration dài bao nhiêu.
- **Nguyên nhân (1):** `handleSaveSectionTab` (16732) ghi đè `duration: durationSec` (tính từ content tab) → mỗi lần lưu tab, section item tự giãn/thu theo content — mất kích thước user đã resize.
- **Trạng thái (2):** ĐÃ ĐÚNG — play MAIN clamp theo `sec.duration` (19154-19156 clip / 19191-19192 midi — activeEnd = min(..., secStart + sec.duration)) + `computeMainSessionEndTime` (254) dùng `s.start + s.duration` (item, không phải content) → playhead dừng đúng theo item. Chỉ cần fix (1) để duration không bị đổi.
- **FIX (app.jsx handleSaveSectionTab):** bỏ ghi đè `duration` (giữ nguyên kích thước item đã resize) — chỉ cập nhật name + tracks; bỏ luôn code tính `durationSec`/`maxEndTime` (dead).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070505), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → resize section item (vd 8s) → mở SECTION-TAB (content dài hơn) → Lưu section (Ctrl+S) → item trên MAIN GIỮ NGUYÊN 8s; play MAIN → chỉ nghe đúng 8s (clamp theo item — content dài hơn không phát tiếp); playhead dừng tại end item.
### [2026-08-07 05:10] Task: MAIN SESSION — playhead vẫn di chuyển khi hết item (chưa nhấn stop) — thêm log chẩn đoán
- **Báo cáo user:** MAIN SESSION — playhead vẫn di chuyển khi chưa nhấn stop; còn item → play âm; hết item → playhead vẫn chạy tiếp.
- **Phân tích:** playhead main dừng tại `projectEnd` (= computeMainSessionEndTime — end item CUỐI, theo s.start + s.duration — KHÔNG buffer). 2 khả năng: (a) hết item nhưng còn item khác dài hơn → playhead chạy qua khoảng trống (âm hết, playhead chạy) — hành vi DAW chuẩn; (b) projectEnd tính SAI (lớn hơn thực tế — item cuối không phải item thật) → playhead chạy quá.
- **FIX (app.jsx):** thêm log chẩn đoán — (1) `[Play] ... projectEnd=X.XX` (startTrackPlayback — projectEnd lúc schedule); (2) `[Stop] projectEnd reached — updatedTime=... projectEnd=...` (khi playhead tới projectEnd — xác nhận có dừng hay không).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070510), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN (section item / midi item) → đợi hết → dán log: `[Play] ... projectEnd=` (giá trị bao nhiêu — có khớp end item cuối không) + có `[Stop] projectEnd reached` không (dừng đúng hay chạy quá).
### [2026-08-07 05:15] Task: MAIN SESSION — playhead chạy qua khoảng im sau khi hết content section item
- **Log user (0510):** `[Stop] projectEnd reached — updatedTime=22.60 projectEnd=22.59` — playhead DỪNG ĐÚNG tại projectEnd — nhưng projectEnd = 22.59 (duration section item — resize thủ công) trong khi CONTENT thực trong tab ngắn hơn (~8s) → im lặng 8→22.59s → user thấy "hết item mà playhead vẫn di chuyển".
- **Nguyên nhân:** projectEnd (main) = computeMainSessionEndTime — section contribution = `s.start + s.duration` (duration ITEM — không phải content thực).
- **FIX (app.jsx projectEnd useMemo):** section contribution = `s.start + min(s.duration, CONTENT end từ tab)` — content ngắn hơn item → projectEnd rút về end content (dừng sớm — hết âm); content dài hơn item → giữ item duration (0505 — MAIN chỉ play duration item). Chỉ áp dụng khi section item là end cuối (max hiện tại).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070515), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN (section item duration 22.59s, content 8s) → playhead DỪNG tại ~8s (hết content — log `[Stop] projectEnd reached — projectEnd≈8`); content dài hơn item → dừng tại item end (cắt như 0505).
### [2026-08-07 05:20] Task: projectEnd chỉ kích hoạt khi nút LOOP bật — play loop liên tục
- **Yêu cầu user:** projectEnd chỉ được kích hoạt khi nút LOOP được bật (cho phép dự án play loop liên tục). LOOP tắt → play 1 lần tới end items (KHÔNG cắt sớm theo content — bỏ hiệu ứng 0515 khi loop tắt).
- **FIX (app.jsx):**
(1) `projectEnd` useMemo — content-min (0515: section end = sec.start + min(item duration, content end)) CHỈ áp dụng khi `isLoopingSelection` (deps thêm isLoopingSelection). LOOP TẮT → projectEnd = computeMainSessionEndTime thuần (end items — play đủ, dừng tại đó).
(2) Nút LOOP transport (25838-25848): BỎ auto-derive selection 0→end items — LOOP bật không selection → loop tại projectEnd (nhánh updatePlayhead `updatedTime >= projectEndRef` loop lại từ 0 — liên tục). User có selection riêng → giữ loop vùng chọn.
- **Hành vi:** LOOP TẮT → playhead chạy tới END ITEMS (22.59) rồi dừng (không cắt 8s content). LOOP BẬT → playhead tới projectEnd (content end ~8s) → LOOP LẠI từ 0 → play liên tục.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070520), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → (1) LOOP TẮT → play MAIN (section 22.59s, content 8s) → play tới 22.59 mới dừng; (2) bấm LOOP → play → loop liên tục tại ~8s (log `[Stop] projectEnd reached — projectEnd≈8` + loop lại từ 0).
### [2026-08-07 05:25] Task: Tăng size Time display (toolbar) + tăng statusbar 150%
- **Yêu cầu user:** tăng size ô hiển thị thời gian (playhead/tổng) trên toolbar; tăng kích thước status bar lên 150%.
- **FIX (app.jsx):**
(1) Toolbar — ô `Time: <playhead> / <total>`: `text-[14px]``text-[20px]` (4 spans: label, playhead, "/", total).
(2) Statusbar — container `h-6``h-9` (36px = 150%), `text-xs``text-[18px]`; các badge trạng thái (Local Sel/Global Sel/Solo Loop/Master Loop) `text-xs``text-[18px]`; Bars selection (Bars/inputs/♪T) + Time selection (Start/End/Len): `text-xs``text-[18px]`, input `w-10``w-[60px]`, `w-16``w-24`, divider `h-4``h-6`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070525), `wiki.md`. Rebuild precompiled (build PASS — text-[20px] ×4, text-[18px] ×18, w-[60px] ×7, h-9 ✓).
- **Ghi chú/Test:** hard refresh → toolbar Time to rõ (20px); statusbar cao hơn + chữ/input 18px (150%).
### [2026-08-07 05:30] Task: Quét chọn (tempo track) không cập nhật bars hiển thị dưới statusbar
- **Báo cáo user:** quét ở tempo track để select duration → vị trí quét + số bars được chọn KHÔNG hiển thị ở bars dưới status bar.
- **Nguyên nhân:** beginBar/endBar/numberBar CHỈ được set từ onChange của input (nhập tay — dock panel cũ + statusbar mới) — KHÔNG có code cập nhật từ sweep/selection → statusbar luôn hiển thị 0/0/1 khi quét.
- **FIX (app.jsx):** thêm effect `[selectionStart, selectionEnd, bpm]` — khi selection hợp lệ (end > start): beginBar = floor(start / secPerBar); endBar = ceil(end / secPerBar) 1; numberBar = endBar beginBar + 1 → quét xong bars statusbar hiển thị đúng vị trí + số bar.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070530), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → quét chọn trên tempo track (vd từ bar 2 tới bar 5) → statusbar hiển thị Bars: 2 - 5 # 4; xóa selection → giữ giá trị cuối (không tự reset).
### [2026-08-07 05:35] Task: Bars hiển thị sai chỉ số — quét 8 bars hiển thị "0 đến 7"
- **Báo cáo user:** số bars đúng (8) nhưng chỉ số sai: quét bar 0→8 hiển thị "0 đến 7" — cần "0 đến 8".
- **Nguyên nhân:** effect (0530) tính `endBar = ceil(end/spb) 1` (bar cuối INCLUSIVE) → 7; numberBar = endBar beginBar + 1 = 8.
- **FIX (app.jsx):**
(1) Effect: `endBar = ceil(end/spb)` (KHÔNG 1 — bar KẾT THÚC vùng quét/chạm vạch); `numberBar = ceil floor` (KHÔNG +1) → quét 8 bars → "0 đến 8 # 8".
(2) Input End Bar (dock 26436 + statusbar 27846): `setSelectionEnd(t + spb)``setSelectionEnd(t)` (selection tới vạch bar b — khớp hiển thị); `setNumberBar(b beginBar)`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070535), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → quét 8 bars từ 0 → BARS hiển thị "0 - 8 # 8"; nhập End Bar = 8 → selection tới vạch 8 (8 bars).
### [2026-08-07 05:40] Task: MAIN SESSION — play section item có âm nhưng track VU không hiển thị
- **Báo cáo user:** play section item ở MAIN — âm phát ra (track) nhưng VU meter không nhảy.
- **Phân tích:** MIDI section (FluidSynth SF) đi thẳng masterBus.input (midiAudible main rỗng → setOutputDestination(null)) — KHÔNG qua node track/sub-node analyser → audioPeak = 0 — VU track chỉ nhảy qua `triggerMidiVuActivity` (midiVuActivityRef). Trigger cũ (19511-19518) VẪN CÒN (fire lúc schedule — không setTimeout cho note delayed). Chưa rõ vì sao VU đứng yên — cần log.
- **FIX (app.jsx):** (1) bổ sung trigger VU cho nhánh note DELAYED (setTimeout khớp nhịp — subTrack.id + track.id) — nhánh instant đã có; (2) thêm log chẩn đoán VU tick: `[VU] <trackId> audio=... midi=... vNode=... analyser=... canvas=...` (rate-limited ~1.3 lần/giây).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070540), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (section item MIDI) → dán log `[VU]` — xác định: midi= có >0 không (trigger fire?); vNode/analyser/canvas có tồn tại không.
### [2026-08-07 05:45] Task: Track VU vẫn diễn animation khi không còn âm (MAIN/SECTION-TAB)
- **Báo cáo user:** track VU meter không diễn đúng — hết âm vẫn nhảy animation.
- **Phân tích:** VU track (MIDI section) chỉ nhảy qua `midiVuActivityRef` (trigger fire lúc schedule). Trigger cũ fire NGAY lúc schedule (kể cả note delay xa — VU nhảy trước âm / note bị cắt vẫn flash) + TRÙNG với trigger mới (05:40) → VU nhảy sai nhịp/kéo dài. Decay 0.9/frame (~0.75s) — sau note cuối VU còn diễn ~0.75s.
- **FIX (app.jsx):**
(1) XÓA trigger cũ fire-ngay (sau block — chạy cho mọi note cả delayed) — chỉ giữ trigger mới: nhánh delayed (setTimeout delay*1000 — khớp nhịp) + nhánh instant.
(2) Decay nhanh: `midiPeak * 0.90``0.72` (tắt sau ~0.35s — hết note → VU dừng nhanh).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070545), `wiki.md`. Rebuild precompiled (build PASS — triggerMidiVuActivity còn 8 chỗ).
- **Ghi chú/Test:** hard refresh → play MAIN (section MIDI) → hết notes → VU dừng trong ~0.35s; nếu VU VẪN nhảy khi hết âm → dán log `[VU]` (midi= giá trị bao nhiêu khi không còn âm).
### [2026-08-07 05:50] Task: VU vẫn animation khi hết âm — log nguồn trigger [VUTrig]
- **Log user (0545):** âm ngừng nhưng `[VU] 1 audio=0.000 midi=0.058``midi=0.661` — midiPeak TĂNG LẠI → triggerMidiVuActivity vẫn fire định kỳ sau khi hết âm.
- **FIX (app.jsx):** thêm log `[VUTrig] <trackId> vel=... peak=... play=<isPlaying> tab=<activeTab>` vào `triggerMidiVuActivity` (rate-limited 1/20 fire) — xác định AI còn fire (setTimeout sót từ main-midi delayed? re-schedule? keyboard preview?).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070550), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (hết âm — VU vẫn nhảy) → dán log `[VUTrig]` — cho biết track nào + lúc nào (play=?) vẫn fire.
### [2026-08-07 05:55] Task: CHỐT — VU vẫn nhảy khi hết âm = setTimeout VU trigger sót sau stop
- **Log user (0550):** sau STOP (`[Play] click isPlaying=true` → stop) — `[VUTrig] 1 vel=0.83/0.78/0.95 ... play= false`**trigger VU vẫn fire sau khi play đã dừng**.
- **Nguyên nhân:** setTimeout VU trigger của note DELAYED (main-midi 19351 + section delayed — khôi phục 05:40) KHÔNG bị hủy khi stop → fire MUỘN sau stop → midiVuActivity set lại → VU nhảy (decay 0.72 vẫn chưa kịp về 0).
- **FIX (app.jsx):** thêm `isPlayingRef` (App — sync mỗi render) + guard `if (!isPlayingRef.current) return;` ở ĐẦU 2 setTimeout VU trigger (main-midi + section delayed) — stop → isPlaying=false → setTimeout sót không fire → VU dừng ngay khi hết âm/stop.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070555), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (MIDI) → STOP giữa chừng → log `[VUTrig]` KHÔNG còn fire sau stop (chỉ fire lúc play= true); hết bài tự nhiên → VU dừng trong ~0.35s.
### [2026-08-07 06:00] Task: VU vẫn nhảy khi playhead ngoài vùng âm — log nguồn peak>0
- **Báo cáo user:** VU vẫn diễn animation khi playhead di chuyển ra khỏi vùng có âm. Log 0555: track VU `midi=0.000` liên tục (đã dừng đúng) — NHƯNG user vẫn thấy nhảy → nguồn KHÁC (master VU? key khác? peak giữa 2 log rate-limited?).
- **FIX (app.jsx):** đổi log chẩn đoán — (1) `[VU] <trackId> audio=... midi=... key=<key>` CHỈ in khi peak > 0.001 (VU nhảy thật — không rate-limit); (2) thêm `[VU-master] peak=...` (master VU khi nhảy) — xác định VU nào còn nhảy ngoài vùng âm.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070600), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN → playhead ra khỏi vùng âm → nếu VU vẫn nhảy → dán log `[VU]`/`[VU-master]` xuất hiện lúc đó (cho biết track/key/nguồn audio hay midi).
### [2026-08-07 06:05] Task: SECTION-TAB — VU nhảy theo velocity trigger (giả) — đọc âm THỰC từ SF output
- **Log user (0600):** SECTION-TAB — click playhead tới vị trí không âm → VU vẫn nhảy: `[VU] 1 midi= 0.98 → 0.85 → ...` (tăng lại theo chu kỳ — trigger fire lúc schedule/re-schedule) nhưng `[VU-master] peak= 0.008` (âm thực gần im) — **VU GIẢ (velocity trigger), không theo âm thực**. Master VU nhảy nhẹ 0.008 liên tục (noise nền).
- **FIX (app.jsx VU tick):**
(1) Tap analyser trên SF output (`SonicSF.getOutputNode()` — âm MIDI THỰC post-FluidSynth) — `sfAudioPeak` gộp vào audioPeak — VU MIDI theo âm THỰC.
(2) Khi SF analyser đã tap → `midiPeak = 0` (bỏ velocity trigger giả) — hết note → SF im → VU 0 ngay (không nhảy giả sau click playhead/re-schedule).
(3) Master VU deadband: `peak < 0.01 → 0` — hết noise nền 0.008.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070605), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → SECTION-TAB play → click playhead tới vị trí không âm → VU KHÔNG nhảy (SF im → 0); vị trí có âm → VU nhảy theo âm thực; master VU không nhảy noise.
### [2026-08-07 06:10] Task: VU vẽ đoạn ngắn (0.008) khi hết âm — deadband track analyser
- **Log user (0605):** `[VU] 1 audio= 0.008 midi= 0.000` — VU vẽ đoạn nhỏ (~-42dB) dù không âm — noise nền trên track analyser (như master 0.008 — đã deadband 06:05).
- **FIX (app.jsx):** track audioPeak deadband — `if (audioPeak < 0.01) audioPeak = 0;` (sau khi gộp sfAudioPeak) — hết âm → VU tắt NGAY (peak 0 → db -60 → vẽ 0).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070610), `wiki.md`. Rebuild precompiled (build PASS — Deadband ×2: master + track).
- **Ghi chú/Test:** hard refresh → play → playhead ra khỏi vùng âm → VU TẮT NGAY (không còn đoạn 0.008); log `[VU]` không còn audio=0.008.
### [2026-08-07 06:15] Task: PIANO ROLL restored (phiên trước) — playhead đứng im khi play MAIN/SECTION
- **Báo cáo user:** đang play MAIN/SECTION → mở lại PIANO ROLL TAB đã mở từ phiên làm việc trước (restored từ project) → playhead đứng im — không đồng bộ vị trí play.
- **Nguyên nhân:** tab restored KHÔNG có `buffer` (AudioBuffer không serialize được) → nhánh updatePlayhead sub-tab `if (!st || !st.buffer) return;` → return sớm → playhead không cập nhật.
- **FIX (app.jsx updatePlayhead nhánh sub-tab):** `if (!st.buffer && st.type !== 'PIANO_ROLL') return;` — PIANO ROLL không cần buffer (chỉ hiển thị playhead — đồng bộ theo vị trí play MAIN/SECTION); `startSubTabPlayback` tự tạo silent buffer khi play. Audio sub-tab vẫn cần buffer (giữ return cũ).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070615), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → load project có piano roll tab mở sẵn → play MAIN/SECTION → mở lại tab piano roll → playhead DI CHUYỂN đúng vị trí play (đồng bộ); bấm play trong tab → vẫn play bình thường (silent buffer tự tạo).
### [2026-08-07 06:20] Task: Mở PIANO ROLL restored lúc MAIN đang play — âm lag ~1s
- **Báo cáo user:** đang play MAIN/SECTION → mở lại PIANO ROLL TAB (restored phiên trước) → âm thanh bị lag ~1s rồi mới phát tiếp.
- **Nguyên nhân:** effect đổi tab → `updateSfRouting` nhánh PIANO_ROLL → `setOutputDestination` (SF _gainNode FluidSynth) — disconnect + reconnect khi âm đang phát → âm MIDI đang kêu bị NGẮT (dedupe chỉ chặn khi đích KHÔNG đổi — đổi masterBus→node piano roll = swap → ngắt).
- **FIX (app.jsx updateSfRouting):** nhánh PIANO_ROLL thêm điều kiện `&& !isPlayingRef.current` — MAIN/SECTION đang play → BỎ QUA nhánh piano (giữ SF route hiện tại — âm tiếp tục); chỉ route vào node piano roll khi KHÔNG play (preview tab — handlePlayPause sub-tab tự stopAllPlayback trước).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070620), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (có âm MIDI) → mở lại piano roll tab restored → ÂM KHÔNG BỊ NGẮT (phát liên tục — không lag); bấm play trong piano roll tab (không play main) → route SF vào node piano roll như cũ (nghe FX track).
### [2026-08-07 06:25] Task: FIX hồi quy 0620 — âm nhỏ/dry khi mở piano roll restored lúc main play
- **Báo cáo user:** bản cũ (0615) lag nhưng âm TO (qua mastering); bản 0620 hết lag nhưng âm NHỎ/không rõ (mất bypass sai).
- **Nguyên nhân:** fix 0620 skip nhánh PIANO_ROLL → rơi xuống nhánh midiAudible (list = section clones khi activeTab=midi_*) → node lookup trúng NODE MAIN '1' (sfEntry) → SF (âm main đang phát) ĐỔI ĐÍCH → sfEntry main → sfRouteGain theo ♪ main (bypass) → âm DRY/nhỏ.
- **FIX (app.jsx updateSfRouting):** guard ĐẦU HÀM — `isPlayingRef.current && activeTab là sub-tab (midi_*/không main/không session)` → RETURN NGAY (KHÔNG đổi SF destination) — âm main đang phát giữ route đã set đúng lúc schedule (masterBus.input — qua mastering — TO). Hết lag (không swap) + hết bypass sai (không route vào node main).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070625), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (âm MIDI TO — qua mastering) → mở lại piano roll tab restored → ÂM VẪN TO (không nhỏ/dry) + KHÔNG lag; bấm play trong piano roll (main không play) → route SF vào node tab như cũ.
### [2026-08-07 06:30] Task: Mở PIANO ROLL vẫn lag (ngắn hơn, 2 lần) — ghost extract block main thread
- **Báo cáo user:** sau 0625 (SF route guard) vẫn lag khi chuyển sang piano roll restored — ngắn hơn + âm lag 2 lần.
- **Nguyên nhân:** PianoRollTabEditor mount → `ghostLayers` useMemo → `extractGhostLayers(activeTracks,...)` duyệt TOÀN BỘ project (tracks + notes) — block main thread 100-500ms → noteon FluidSynth (scheduled qua setTimeout) TRỄ → âm lag (2 noteon trễ = "lag 2 lần").
- **FIX (app.jsx PianoRollTabEditor ghostLayers):** đang play (`isPlaying` prop — main/section) → return [] (BỎ ghost — chỉ là hiển thị tham chiếu, không cần khi âm chạy) — không block → âm không lag. Khi dừng play → tính ghost bình thường (không âm — block OK). Deps thêm isPlaying.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070630), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN → mở piano roll tab restored → ÂM KHÔNG LAG (to, liên tục); dừng play → ghost notes hiển thị lại bình thường.
### [2026-08-07 06:35] Task: CHỐT VU MIDI — analyser tap bị disconnect() ngắt → VU không nhảy khi có âm
- **Báo cáo user:** (1) [lặp] click playhead → vị trí không âm → VU vẫn diễn (log cũ 0600 — velocity trigger giả); (2) MỚI — play MIDI trên track → KHÔNG có VU animation.
- **Nguyên nhân (chốt cả 2):** VU tick (0605) tap analyser trên `_gainNode` SF (âm MIDI thực) — NHƯNG `setOutputDestination` (soundfontPlayer.js 85) gọi `_gainNode.disconnect()` KHÔNG đối số → ngắt MỌI kết nối KỂ CẢ analyser tap → `sfAudioPeak` = 0 MÃI → (2) VU MIDI không nhảy khi có âm; và (1) `_sfTapped` vẫn true → velocity giả bị bỏ nhưng âm thực = 0 → VU chết hoàn toàn.
- **FIX (soundfontPlayer.js):** `setOutputDestination` — sau `_gainNode.connect(dest)``if (_gainNode.__vuAnalyser) _gainNode.connect(_gainNode.__vuAnalyser);` — analyser sống → sfAudioPeak đọc âm MIDI THỰC → VU nhảy khi có âm ✓; velocity giả đã bỏ (midiPeak=0 khi tapped) → hết nhảy giả khi click playhead vị trí không âm ✓.
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js` (bump ?v=202608070635 trong index.html), `wiki.md`. (app.precompiled giữ 0630 — không đổi logic app.jsx).
- **Ghi chú/Test:** hard refresh → (1) play MIDI track → VU nhảy theo âm thực; (2) click playhead tới vị trí không âm → VU tắt ngay; (3) mở piano roll tab → âm to không lag.
### [2026-08-07 06:40] Task: Vào SECTION-TAB lúc MAIN play → CHỈ play section items (tắt âm MAIN items)
- **Yêu cầu user:** play MAIN → di chuyển sang SECTION-TAB → ONLY play items của SECTION-TAB — tắt âm thanh của MAIN items.
- **FIX (app.jsx effect đổi tab 14814):** prevTab='main' → activeTab=`session_*` + đang play (`isPlaying`) → `stopAllPlayback()` (dừng MAIN items) + `startTrackPlayback(_pt)` với activeTracks = section clones (activeTab mới) — play CHỈ section tab items. `_pt` = vị trí play chuyển sang LOCAL của tab (trừ `secStart` — vị trí section trên main — tìm qua `tracks[].sections[]` bằng sectionId) — section items dùng time local 0-based.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070640), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (có main items + section item) → vào SECTION-TAB → CHỈ nghe section items (âm main items tắt — log `[SectionTab] play CHỈ section items tại local X.XX (secStart Y.YY)`); playhead section chạy từ vị trí tương ứng.
### [2026-08-07 06:45] Task: Tắt âm MAIN items (vào SECTION-TAB) → VU phải tắt theo
- **Yêu cầu user:** 0640 đã tắt âm MAIN items khi vào SECTION-TAB ✓ — NHƯNG VU animation của âm items đó VẪN diễn — cần tắt VU.
- **Nguyên nhân:** `midiVuActivityRef` (velocity trigger) còn giá trị cũ — decay 0.72/frame (~0.35s) sau stop → VU main items "diễn" tiếp dù âm đã tắt.
- **FIX (app.jsx stopAllPlayback):** clear `midiVuActivityRef.current = {}` NGAY khi stop — VU tắt tức thì (không decay từ âm cũ). VU sau đó chỉ theo âm THỰC (SF analyser — fix 0635): tab items phát → VU nhảy; im → VU 0.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070645), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (âm main items) → vào SECTION-TAB → âm main tắt + VU main tắt NGAY (không diễn tiếp); tab items phát → VU tab nhảy theo âm thực.
### [2026-08-07 06:50] Task: VU track nhảy nhẹ (0.016/0.023) khi không âm — noise SF analyser
- **Log user (0645):** track 1 + 2 `audio= 0.016/0.023` (CÙNG giá trị — sfAudioPeak — SF analyser noise nền) nhảy nhẹ dù không âm track (master 0.25 = âm thật clip — track VU không liên quan); deadband 0.01 KHÔNG chặn 0.016.
- **FIX (app.jsx):** deadband track `audioPeak < 0.02 → 0` (nâng từ 0.01) — chặn noise SF 0.016/0.023 — VU tắt khi âm < 0.02 (≈ -34dB — âm thật thường lớn hơn).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070650), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play → vị trí không âm → VU track KHÔNG nhảy (0.016 bị chặn); có âm thật (> 0.02) → VU nhảy; master VU giữ deadband 0.01 (âm thật nhỏ vẫn hiển thị).
### [2026-08-07 06:55] Task: VU track = noise (0.023) khi âm MIDI thật 0.25 — guard SF block skip
- **Log user (0650 — SECTION-TAB):** track 1+2 `audio= 0.023/0.031` (noise vNode analyser) TRONG KHI `[VU-master] peak= 0.25` (âm MIDI thật phát — noteon) — VU track không theo âm thật.
- **Nguyên nhân:** guard `if (audioPeak <= 0.001)` quanh block đọc SF analyser — vNode noise 0.023 > 0.001 → block SF SKIP → sfAudioPeak = 0 → VU = noise vNode 0.023 (nhảy nhẹ mãi) — âm MIDI thật (SF → masterBus 0.25) không được đọc.
- **FIX (app.jsx):** (1) LUÔN đọc SF analyser (bỏ guard — sfAudioPeak gộp max vào audioPeak) — VU MIDI theo âm thật; (2) deadband `0.02 → 0.03` — chặn noise 0.023-0.031.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070655), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → chuyển SECTION-TAB (play) → VU track theo âm thật (MIDI 0.25 — nhảy đúng); vị trí không âm → VU tắt (noise 0.023 bị deadband 0.03 chặn).
### [2026-08-07 07:00] Task: Track VU nhảy CÙNG NHAU — bỏ sfAudioPeak gộp chung
- **Log user (0655):** track 1 + 2 `audio= 0.031/0.039` — CÙNG GIÁ TRỊ — nhảy cùng nhau. User muốn: track CÓ âm → nhảy; KHÔNG âm → không (PHÂN BIỆT track).
- **Nguyên nhân:** sfAudioPeak (SF = 1 output CHUNG cho mọi track MIDI) gộp vào audioPeak mọi track → track VU cùng giá trị/nhảy cùng nhau; track audio cũng bị SF chi phối.
- **FIX (app.jsx VU tick):** (1) KHÔNG gộp sfAudioPeak vào audioPeak — audioPeak chỉ theo âm TRACK thật (vNode/sub-node analyser — clip audio phân biệt); (2) MIDI track VU = velocity trigger (midiVuActivity — PHÂN BIỆT track — track nào có note → nhảy) + GATE theo âm SF THỰC (sfAudioPeak < 0.03 → midiPeak = 0 — SF im = hết note/click playhead vị trí không âm → VU tắt ngay — hết nhảy giả).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070700), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play SECTION-TAB → track MIDI (note) nhảy theo velocity khi SF phát; track audio (clip) nhảy theo analyser riêng; track không âm → đứng yên (KHÔNG cùng nhau); click playhead vị trí không âm → VU tắt ngay (SF im → gate).
### [2026-08-07 07:05] Task: Track không item VU vẫn nhảy + VU không đúng nhịp
- **Log user (0700):** [VU] 1 midi= velocity (0.66→1.0 — gate SF mở ✓); track 2 không log (VU 0 ✓). 2 vấn đề: (1) track KHÔNG item vẫn nhảy nhẹ — noise vNode 0.031-0.039 qua deadband 0.03; (2) velocity decay 0.72/frame quá nhanh — VU nhấp nháy không theo âm (sustain) — "không đúng nhịp".
- **FIX (app.jsx VU tick):**
(1) Deadband audioPeak `0.03 → 0.04` — chặn noise 0.031-0.039 — track không item → VU đứng yên.
(2) MIDI VU: bỏ decay nhanh — GATE + GIỮ theo âm SF thực: SF đang phát (sfAudioPeak ≥ 0.03) → midiPeak = velocity (VU theo note, giữ trong lúc âm — đúng nhịp); SF im → midiPeak = 0 + clear midiVuActivityRef (VU tắt ngay).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070705), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play SECTION-TAB → track có note → VU sáng theo note + giữ khi âm kêu (đúng nhịp); track không item → đứng yên; hết âm/click playhead vị trí không âm → VU tắt ngay.
### [2026-08-07 07:10] Task: Note đơn/velocity thấp không có VU — bỏ gate amplitude SF
- **Báo cáo user:** lọc noise quá khắt khe — play note đơn (velocity thấp) → KHÔNG có VU meter (âm SF nhỏ < gate 0.03 → midiPeak = 0).
- **FIX (app.jsx VU tick):**
(1) BỎ gate amplitude SF (sfAudioPeak < 0.03 → midiPeak = 0) — MIDI VU = velocity trigger (fire ĐÚNG NHỊP note: nhánh delayed setTimeout + nhánh instant) — note đơn/velocity thấp → VU nhảy theo velocity ✓.
(2) Decay chậm 0.85 (~1s) — note đơn hiển thị rõ, không nhấp nháy quá nhanh (đúng nhịp âm).
(3) Bỏ luôn block đọc SF analyser (không còn dùng — tiết kiệm CPU/frame).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070710), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play note đơn (velocity thấp) → VU NHẢY (theo velocity); nhóm note → VU nhảy; không âm (vị trí im) → VU tắt sau ~1s; track không item → đứng yên (deadband audioPeak 0.04 — chỉ chặn noise track, không ảnh hưởng MIDI velocity).
### [2026-08-07 07:15] Task: Track 2 section (không item) VU vẫn nhảy + decay quá lâu
- **Báo cáo user:** MAIN — track 1 section item, track 2 midi item. Vào SECTION-TAB — track 1 có midi item, track 2 (cùng duration) KHÔNG item — NHƯNG VU track 2 vẫn diễn; mức diễn không đúng velocity/decay.
- **Nguyên nhân:** (1) noise vNode analyser dao động quanh 0.04 — qua deadband 0.04 → track không item VU nhảy nhẹ (không theo velocity — lung tung); (2) decay 0.85 (~1s) quá lâu — sau hết âm VU "vẫn diễn" ~1s.
- **FIX (app.jsx VU tick):** (1) deadband audioPeak `0.04 → 0.05` — chặn noise quanh 0.04 — track không item đứng yên; (2) decay `0.85 → 0.75` (~0.5s) — note đơn thấy rõ nhưng tắt nhanh sau hết note; (3) log `[VU]` in MỌI key (kể cả peak 0 — rate ~1.3/s — kèm key/peak) — xác nhận nguồn.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070715), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN → vào SECTION-TAB → track 2 (không item) VU ĐỨNG YÊN; track 1 VU theo velocity + tắt ~0.5s sau hết note; nếu track 2 VẪN nhảy → dán log `[VU]` (audio= bao nhiêu — noise hay midi= còn giá trị).
### [2026-08-07 07:20] Task: TÁCH VU meter MAIN SESSION vs SECTION-TAB (item tab nào → VU tab đó)
- **Yêu cầu user:** tách biệt VU meter MAIN vs SECTION-TAB — item của tab nào thì play VU của tab đó (gốc rễ: section clone id TRÙNG main id → VU canvas/trigger key lẫn nhau).
- **FIX (app.jsx):**
(1) Track strip VU canvas — section clone (`track._isSectionClone`) dùng key `'_sess_' + track.id` (main giữ `track.id`).
(2) VU tick — parse key: `_sess_<id>` → trackId = id thuần (node lookup), vuKey = key đầy đủ (midiVuActivityRef theo context).
(3) Trigger MIDI (startTrackPlayback — 4 chỗ main/section clones): fire key theo CONTEXT — activeTab session → `'_sess_' + id`; main → id thuần — mixer/main strip (key id) không nhận trigger section; section strip (key _sess_<id>) chỉ nhận trigger section.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070720), `wiki.md`. Rebuild precompiled (build PASS — _sess_ ×8).
- **Ghi chú/Test:** hard refresh → MAIN play (main items) → VU main nhảy; vào SECTION-TAB (section items play) → VU section nhảy — VU main (mixer) ĐỨNG (không nhận trigger section); track section không item → đứng yên.
### [2026-08-07 07:25] Task: Cut/Copy MIDI item (context menu) → paste ra audio item (SAI)
- **Báo cáo user:** Cut MIDI item bằng context menu → paste ra AUDIO item (track có midi item vị trí 1 + audio item vị trí 2) — copy nhầm.
- **Nguyên nhân:** context menu chỉ biết TRACK (contextMenu = {trackId, time, sectionId}) — Copy/Cut (17542/17593) chỉ xử lý track buffer (audio) → cut midi item → copy nhầm buffer track → paste = audio item.
- **FIX (app.jsx):**
(1) TimelineTrack onContextMenu — detect ITEM dưới chuột: MIDI item (midiItems + duration bounds) + clip (clips + buffer) → truyền `itemId` + `itemType` ('midi'/'clip').
(2) handleContextMenu — nhận itemId/itemType → lưu vào contextMenu state.
(3) contextMenuCopy/Cut — xử lý ITEM TRƯỚC track: MIDI → clipboard {type:'midi', notes, duration(sec), name, color} — paste = MIDI item (doPaste nhánh midi đã có); clip → clipboard {buffer,...}; Cut = copy + XÓA item (updateActiveTracks filter).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070725), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → chuột phải MIDI item → Cut → Paste → ra MIDI ITEM (không phải audio); chuột phải clip → Copy → Paste → clip; chuột phải track (không item) → Copy → track buffer như cũ.
### [2026-08-07 07:30] Task: (1) Lưu project phải lưu tất cả tab — XÁC NHẬN đã đúng (2) Chuyển PIANO ROLL tab → stop âm tab cũ
- **Yêu cầu (1):** ở MAIN bấm lưu/Ctrl+S phải lưu tất cả các tab.
- **Xác nhận:** serializeProjectToSchema ĐÃ lưu đủ — sessionTabs → section_store (tracks đầy đủ) + sub_tabs (id/trackId/target_id/parent_tab_id/notes/currentTime); deserialize restore đủ (9327-9358). Ctrl+S (16295) + nút Save Project (25368) → handleSaveProject → serialize đủ. KHÔNG cần sửa — nếu user thấy thiếu tab sau load → dán schema đã lưu.
- **Yêu cầu (2):** chuyển PIANO ROLL TAB 1 → TAB 2 phải play đúng nội dung từng tab.
- **Nguyên nhân:** rời piano roll tab đang play (luật 04:15 "không stop") → sang tab 2 → âm tab 1 tiếp tục (nghe nhầm nội dung tab 1 khi ở tab 2).
- **FIX (app.jsx effect 14814):** prevSub PIANO_ROLL đang play + activeTab MỚI là SUB-TAB (midi_*/không main/session) → stopAllPlayback (dừng âm tab cũ — play tab mới đúng nội dung). Về MAIN/SECTION → giữ luật cũ (âm tiếp/resume).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070730), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play piano roll tab 1 → chuyển tab 2 → ÂM TAB 1 DỪNG (im — bấm play tab 2 → nghe tab 2); về MAIN → hành vi cũ.
### [2026-08-07 07:35] Task: (1) Lưu ở MAIN phải lưu dirty tabs (2) Xóa midi item → close piano roll tab tương ứng
- **Báo cáo user:** (1) nhấn lưu ở MAIN SESSION không lưu dirty tab (PIANO ROLL đã sửa notes); (2) xóa midi item phải close PIANO ROLL TAB đã mở của item đó.
- **Nguyên nhân (1):** keydown Ctrl+S handler thứ 2 (16388) — nhánh `curTab === 'main'` RỖNG ("handled by main handler" — không làm gì) → MAIN Ctrl+S không save project + không lưu dirty; nút menu Save Project (25370) chỉ gọi handleSaveProject (không lưu dirty).
- **FIX (app.jsx):**
(1) Ctrl+S (16388): nhánh main/session → `handleSaveProject()` + save TẤT CẢ dirty sub-tabs (PIANO_ROLL → handleSaveMidiNotes; SECTION → saveSectionTab; audio → buffer về track); nút Save Project → cùng logic.
(2) Xóa midi item (contextMenuDelete 17480 + contextMenuCut 17666) → `setSubTabs(filter target_id !== itemId)` — close piano roll tab của item; nếu tab đang active bị close → `setActiveTab('main')`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070735), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → sửa notes piano roll (dirty) → về MAIN → Ctrl+S / Save Project → reload → notes ĐÃ LƯU (tab vẫn mở + nội dung mới); xóa midi item → piano roll tab của item đó tự close (+ về main nếu đang active tab đó).
### [2026-08-07 07:40] Task: SECTION-TAB — xóa midi item (Delete key) → close piano roll tab tương ứng
- **Báo cáo user:** trong SECTION-TAB, xóa midi item phải close PIANO ROLL TAB đã mở của item đó.
- **Nguyên nhân:** đường xóa chính là `handleDeleteSelectedItemsRef` (Delete key — chọn item → Delete) — xóa midiItems nhưng KHÔNG close subTab (target_id = item id) — áp cho cả MAIN lẫn SECTION (updateActiveTracks theo context).
- **FIX (app.jsx handleDeleteSelectedItemsRef):** đầu hàm — `setSubTabs(filter: bỏ tab có target_id ∈ idsToDelete)` — close piano roll tab của midi item bị xóa; tab đang active bị close → `setActiveTab('main')`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070740), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → SECTION-TAB chọn midi item → Delete → piano roll tab của item đó TỰ CLOSE (về main nếu đang ở tab đó); MAIN tương tự.
### [2026-08-07 07:45] Task: (1) FIX Cut midi → paste audio (detect bounds sai) (2) Xóa item SECTION-TAB → sync canvas main + giữ tab
- **Báo cáo (1) [lặp]:** cut midi item (context menu) → paste ra audio item — fix 0725 chưa hiệu lực.
- **Nguyên nhân (1):** onContextMenu detect MIDI bounds dùng `(m.duration || 4)` KHÔNG × secondsPerBar (m.duration tính BEATS — contextMenuDelete 17452 có ×) → detect miss → itemType null → Cut/Copy xử lý TRACK buffer (audio). **FIX:** bounds × `_spb` (60/bpm*4).
- **Báo cáo (2) [mới]:** xóa item trong SECTION-TAB → phải cập nhật canvas section item ở MAIN + giữ vị trí SECTION-TAB (không quay về MAIN khi close tab).
- **FIX (2):** (a) helper `syncSectionTabToMain(tabId)` — sync `tab.tracks``sec.tracks` trên MAIN (canvas section item vẽ lại — KHÔNG đổi duration 0505); gọi sau mọi xóa item (Delete key handleDeleteSelectedItemsRef + contextMenuDelete: section/midi/clip + contextMenuCut). (b) điều kiện "về main" khi close tab — CHỈ khi activeTab là `midi_*` (piano roll) bị close — KHÔNG đá SECTION-TAB về main.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070745), `wiki.md`. Rebuild precompiled (build PASS — syncSectionTabToMain ×6).
- **Ghi chú/Test:** hard refresh → (1) chuột phải MIDI item (track có midi + audio) → Cut → Paste → MIDI item (không audio); (2) SECTION-TAB xóa item → section item trên MAIN vẽ lại (nội dung mới) + VẪN Ở SECTION-TAB (không về main).
### [2026-08-07 07:50] Task: (2) Sync canvas MAIN realtime sau cut/delete SECTION-TAB (3) Ctrl+X/C phải cut/copy ITEM
- **Báo cáo user:** (2) cut/delete item SECTION-TAB 1 → quay MAIN → phải cập nhật realtime canvas section item 1 (không cần save); (3) Ctrl+X không cut được item.
- **Nguyên nhân:** (2) syncSectionTabToMain (0745) chạy NGAY (TRƯỚC updateActiveTracks — state chưa áp) → sync data CŨ → canvas cũ; (3) Ctrl+C/X (16344/16349) LUÔN handleCopyTrack/handleCutTrack (track) — không xử lý item được chọn.
- **FIX (app.jsx):**
(2) syncSectionTabToMain — gọi qua `setTimeout(0)` (SAU updateActiveTracks — data mới) ở mọi điểm xóa (handleDeleteSelectedItemsRef + contextMenuDelete section/midi/clip + contextMenuCut).
(3) Ctrl+C/X — có ITEM được chọn (selectedItemIds) → `findItemContext` + `copyItemToClipboard` (MIDI notes / clip buffer — helpers dùng chung) → copy item; Ctrl+X → copy + `handleDeleteSelectedItemsRef` (xóa + close piano roll tab + sync section); không chọn item → fallback track cũ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070750), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → (2) SECTION-TAB cut/delete item → quay MAIN → section item canvas CẬP NHẬT realtime (nội dung mới — không save); (3) chọn MIDI item → Ctrl+C → Ctrl+V → paste MIDI item; chọn item → Ctrl+X → item bị cắt (clipboard) + tab close + canvas sync.
### [2026-08-07 07:55] Task: (2) Paste midi block ngắn — duration sai (3) Ctrl+V paste vào track 1
- **Báo cáo user:** (1) [lặp] cut midi → paste audio — fix 0745 chờ xác nhận bản mới; (2) context menu cut midi → paste → item block NGẮN (không giữ duration); (3) Ctrl+V paste vào TRACK 1 (không phải track được chọn).
- **Nguyên nhân:** (2) midiItem.duration tính GIÂY (insertMidiItem = 4*spb; scheduling 19331 dùng seconds) — nhưng copy midi (copyItemToClipboard + contextMenuCopy/Cut) nhân thêm `(60.0/bpm)` → duration gấp đôi → block sai. (3) handlePasteTrack = `doPaste(selectedTrackId, ...)` — chọn ITEM không set selectedTrackId (giữ '1') → paste track 1.
- **FIX (app.jsx):**
(2) copy midi duration = `item.duration || 4` (GIÂY — bỏ × 60/bpm) — 3 chỗ (helper + contextMenuCopy + contextMenuCut) + bỏ biến bpmVal thừa.
(3) handlePasteTrack — có ITEM được chọn (selectedItemIds) → target = track chứa item (findItemContext); ngược lại selectedTrackId.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070755), `wiki.md`. Rebuild precompiled (build PASS — type:'midi' ×4, duration ×3).
- **Ghi chú/Test:** hard refresh → (2) context menu cut midi → paste → block GIỮ ĐÚNG duration; (3) chọn item trên track 2 → Ctrl+V → paste vào track 2 (không phải track 1); (1) cut midi → paste → MIDI item.
### [2026-08-07 08:00] Task: (1) Ctrl+X bị browser capture (2) Ctrl+V paste sai track 1
- **Báo cáo user:** (1) Ctrl-X bị browser capture — không cut item được; (2) Ctrl+V vẫn paste track 1 — phải paste vào track đang select.
- **Nguyên nhân:** (1) Ctrl+X handler thiếu stopPropagation (browser cut text chiếm); (2) click ITEM — mousedown nhánh hitItem `return` sớm (2919) → `onTrackLaneMouseDown` (setSelectedTrackId) KHÔNG chạy → selectedTrackId giữ track cũ ('1') → Ctrl+V paste sai.
- **FIX (app.jsx):** (1) Ctrl+X — thêm `e.stopPropagation()` (cùng preventDefault). (2) click item (mousedown hitItem) → `onSelectTrack(track.id)` — chọn track chứa item → Ctrl+V paste đúng track (handlePasteTrack đã ưu tiên item → fallback selectedTrackId).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070800), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → (1) chọn item → Ctrl+X → item bị cắt (không bị browser chiếm); (2) chọn item trên track 2 → Ctrl+V → paste vào track 2 (đúng track đang select).
### [2026-08-07 08:05] Task: (1) Ctrl-X không cắt item (2) paste tại vị trí click (3) paste sai track — closure stale
- **Báo cáo user:** (1) click item → Ctrl-X không thấy cắt; (2) paste nên tại vị trí con trỏ CLICK (bên phải playhead → paste tại đó; bên trái → paste tại playhead); (3) Ctrl+V vẫn paste track đầu tiên.
- **Nguyên nhân:** (3) keydown effect `deps []` — closure giữ handlePasteTrack RENDER ĐẦU → `selectedTrackId` stale ('1') → paste track 1 LUÔN (bất kể track đang chọn). (2) doPaste luôn dùng currentTime (playhead). (1) chưa rõ — thêm log.
- **FIX (app.jsx):**
(1) Ctrl+X — thêm `console.log('[CutItem] Ctrl+X — itemId=... found=...')` — xác định đường chạy (item hay fallback track).
(2) `lastClickTimeRef` — set trong handlePlayheadSet (click timeline/ruler) — handlePasteTrack: pasteTime = click (nếu > playhead) else playhead.
(3) `handlePasteTrackRef` (ref mới mỗi render) — Ctrl+V gọi ref (hết stale selectedTrackId).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070805), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → (1) chọn item → Ctrl+X → nếu không cắt → dán log `[CutItem]`; (2) click timeline bên phải playhead → Ctrl+V → paste tại chỗ click; click bên trái → paste tại playhead; (3) chọn item track 2 → Ctrl+V → paste track 2.
### [2026-08-07 08:10] Task: Ctrl+X không cắt item — copyItemToClipboard closure stale (activeTracks)
- **Log user (0805):** `[CutItem] Ctrl+X — itemId= midi_1786099252336 found= 2/midi selSize= 1` — handler chạy, detect đúng item — NHƯNG timeline không cắt.
- **Nguyên nhân:** Ctrl+C/X chạy trong keydown effect `deps []` → closure giữ `copyItemToClipboard` RENDER ĐẦU — `activeTracks` (state) là ARRAY CŨ — item `midi_1786099252336` TẠO SAU render đầu → `activeTracks.find` KHÔNG có item → copy fail → fallback `handleCutTrack` (cắt track — không cắt item).
- **FIX (app.jsx copyItemToClipboard):** `const trk = (activeTracksRef.current || []).find(...)` — dùng REF (luôn mới) thay closure state.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070810), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → chọn item (mới tạo) → Ctrl+X → item bị CẮT (log [CutItem] found= track/type + toast 'Đã cắt item'); Ctrl+C → copy item → Ctrl+V paste đúng.
### [2026-08-07 08:15] Task: Kéo ngắn midi item SECTION-TAB → MAIN canvas vẫn 8 bars
- **Báo cáo user:** midi item 2 (8 bars content) kéo NGẮN còn 4 bars trong SECTION-TAB — MAIN section item canvas vẫn hiển thị 8 bars.
- **Nguyên nhân:** syncSectionTabToMain (0745) chỉ chạy khi XÓA item — kéo NGẮN (resize) / MOVE item KHÔNG sync → sec.tracks giữ duration/vị trí cũ → canvas MAIN vẽ 8 bars.
- **FIX (app.jsx):** thêm sync sau thao tác (activeTab session → setTimeout(0) → syncSectionTabToMain): (a) handleMouseUp của RESIZE item (21651 — kéo edge — duration mới); (b) handleMouseUp của MOVE item (21563 — vị trí mới).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070815), `wiki.md`. Rebuild precompiled (build PASS — syncSectionTabToMain ×8).
- **Ghi chú/Test:** hard refresh → SECTION-TAB kéo ngắn midi item 2 (4 bars) → quay MAIN → section item canvas hiển thị ĐÚNG 4 bars (không còn 8 bars); kéo di chuyển item → MAIN canvas theo vị trí mới.
### [2026-08-07 08:20] Task: MAIN canvas section item vẫn 8 bars — notes không clip theo duration
- **Test user:** kéo ngắn midi item 2 (4 bars) SECTION-TAB → MAIN canvas KHÔNG hiển thị 4 bars (vẫn 8).
- **Nguyên nhân:** trim (kéo ngắn) KHÔNG cắt notes (notes 8 bars vẫn trong item — duration giảm 4) — canvas MAIN vẽ section content: `notes.forEach` vẽ MỌI note (2579) KHÔNG clip theo `item.duration` → 8 bars vẫn hiển thị (sync 08:15 đã cập nhật sec.tracks — nhưng vẽ không clip).
- **FIX (app.jsx canvas MAIN — drawSection subMidi notes):** skip note `noteStartSec >= (item.duration || 0) - 0.01` — note ngoài duration (phần đã trim) KHÔNG vẽ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070820), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → SECTION-TAB kéo ngắn midi item (4 bars) → quay MAIN → section item canvas hiển thị ĐÚNG 4 bars (phần 5-8 trống); scheduling đã skip note ngoài duration (19331).
### [2026-08-07 08:25] Task: PIANO ROLL — ctrl+draw velocity chord (nhiều notes cùng vị trí) chỉ vẽ được 1 note
- **Báo cáo user:** velocity lane — notes CÙNG vị trí (chord) khó draw — draw qua vị trí phải vẽ lại TẤT CẢ velocity (nếu notes không được chọn).
- **Nguyên nhân:** findCCNoteIndex trả 1 note (gần stemTop nhất) → ctrl+draw chỉ đổi 1 note — chord notes còn lại giữ nguyên.
- **FIX (app.jsx PianoRollTabEditor):** helper `findCCNoteIndicesAtBeat(b)` — trả TẤT CẢ indices notes có start_beat == snapped — handleCCMouseDown (nhánh KHÔNG chọn): lastPainted = all-at-beat; handleCCMouseMove (non-selected): vẽ TẤT CẢ unpainted notes tại beat (velocity/pan). Selected mode giữ nguyên (chỉ vẽ notes chọn).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070825), `wiki.md`. Rebuild precompiled (build PASS — findCCNoteIndicesAtBeat ×3).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — chord notes cùng beat — ctrl+draw qua cột velocity → TẤT CẢ notes cùng beat đổi velocity (không chọn); có notes chọn → chỉ notes chọn đổi.
### [2026-08-07 08:30] Task: Tích hợp MIDI Toolkit vào Piano Roll Editor (spec 20:12)
- **Spec:** Scale Highlight/Lock, Arpeggiator, Strummer, Humanize, Chord Generator, Velocity Shaper — tích hợp trực tiếp Piano Roll (không qua FX Rack).
- **ĐÃ CÓ SẴN:** snapPitchToScale + selectedScale (menu chuột phải), snapToScale toggle, humanize basic (7144).
- **THÊM MỚI (app.jsx PianoRollTabEditor):**
1. **Scale:** state `scaleRoot` (C..B); toolbar [Scale ▾][Root ▾][Force] — flatten SCALES; `scaleWithRoot()` (transpose); **highlight grid** (rows in-scale sáng vàng / out-of-scale dim — canvas 7394); `applyForceToScale` (notes chọn/all → snap pitch).
2. **Arpeggiator:** modal (pattern UP/DOWN/UP-DOWN/RANDOM/CHORD, rate 1/4-1/32 + triplet/dotted, octaves 1-4, gate 10-200%) — `applyArpeggiate` (chord notes → sequential).
3. **Strummer:** modal (ms 0-120, direction DOWN/UP/ALTERNATE) — `applyStrum` (group same start_beat, sort pitch, Δt + velocity taper 0.03/note).
4. **Humanize:** modal (timing ±ms, vel ±, dur ±%) — `applyHumanizeModal` (jitter start_beat/velocity/duration) — nút Humanize cũ + Alt+R mở modal.
5. **Chord:** [Chord ▾] Triad/Min7/Sus4/Add9 + [Stamp] mode (click canvas → `applyChordStamp` full chord) + Harmonize +3/+5/+7 (`applyHarmonize`).
6. **Velocity:** [Vel: Comp][target][Norm] — `applyVelocityCompress` (shift về target mean), `applyVelocityNormalize` (max → 127).
7. **Shortcuts:** Alt+A (Arp), Alt+S (Strum), Alt+R (Humanize), Shift+C (Stamp), Shift+S (Lock scale toggle).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070830), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → mở PIANO ROLL → chọn scale (toolbar) → grid highlight; chọn notes → Arp/Strum/Humanize modal → Apply; [Stamp] + click canvas → chord; chọn notes → Comp/Norm velocity; Force đưa notes về scale.
### [2026-08-07 08:35] Task: Toolbar Piano Roll — gom 2 hàng + merge tool trùng
- **Yêu cầu user:** toolbar có công cụ TRÙNG — move + merge vào HÀNG DƯỚI.
- **Thực hiện (app.jsx):**
(1) Move khối [Snap to Scale toggle + Scale: ▾/Root ▾/Force + Tools: Arp/Strum/Humanize + Chord: ▾/Stamp/+3/+5/+7 + Vel: Comp/target/Norm] từ giữa hàng 1 → CUỐI toolbar (hàng 2 — sau nút "🎵 Chuyển giọng") với spacer `flex-basis:100%` ép xuống hàng mới (container vốn flex-wrap 2 hàng).
(2) MERGE trùng: nút "🎚 Humanize + strength" cũ (hàng 1) — XÓA (thay bằng Tools:Humanize modal hàng 2 — modal đã có tham số timing/vel/dur).
(3) Hàng 1 giờ gọn: track select, Snap, ARM, Input, Instrument, AI, CC mode, Vel/Sus/Mod/Bend/Pan, Session/Isolated, Copy/Paste/Undo/Redo/Chuyển giọng, Lưu/Export.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070835), `wiki.md`. Rebuild precompiled (build PASS — humstr đã xóa).
- **Ghi chú/Test:** hard refresh → mở PIANO ROLL → toolbar 2 hàng (hàng 1: cơ bản; hàng 2: Scale/Tools/Chord/Vel) — không còn trùng Humanize.
### [2026-08-07 08:40] Task: Toolbar PIANO ROLL — bỏ Copy/Paste, thêm ARP/STRUM/HUMANIZE, gộp 1 hàng
- **Yêu cầu user:** bỏ 2 nút COPY/PASTE (dùng Ctrl-C/X/V + context menu thay); đặt 3 nút ARP/STRUM/HUMANIZE + [dropdown mức humanize] vào chỗ vừa xóa; di chuyển các thành phần còn lại lên cùng hàng với dãy nút này, phía sau nút Chuyển giọng.
- **Thực hiện (app.jsx PianoRollTabEditor toolbar):**
(1) Xóa nút 📋 Copy + 📥 Paste (8882-8900 cũ) → thay bằng ARP/STRUM/HUMANIZE + select mức (Nhẹ 0.05/Vừa 0.10/Mạnh 0.18 — chọn mức → setHumanizeStrength + mở modal theo mức: timingMs=round(v*100), velRange=round(v*127), durRange=round(v*50)).
(2) Xóa spacer flexBasis:100% (ép hàng 2) + xóa khối "Tools: Arp/Strum/Humanize" trùng (hàng 2) → toolbar GỘP 1 HÀNG: ... Chuyển giọng → [Snap to Scale][Scale ▾][Root ▾][Force][Chord ▾][Stamp][+3/+5/+7][Vel: Comp/Norm] → Lưu/Export.
(3) Ctrl+C/X/V trong PIANO ROLL TAB (keydown 7106): C = copy notes (onCopyNotes + toast), X = copy + xóa hết notes, V = paste clipboardNotes (append — như nút cũ) — deps thêm clipboardNotes (handler re-create khi clipboard đổi).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070840), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → mở PIANO ROLL → toolbar 1 hàng (không còn Copy/Paste); ARP/STRUM/HUMANIZE + mức humanize hoạt động (mở modal); Ctrl+C → Ctrl+V paste notes; Ctrl+X cắt notes; Alt+A/S/R mở tool.
### [2026-08-07 08:45] Task: (1) Lưu + Export MIDI lên trước vị trí nút Đóng, bỏ Đóng (2) Velocity không cập nhật realtime khi đổi độ rộng/bật tắt thanh velocity
- **Yêu cầu user:** (1) mang 2 nút Lưu + Export MIDI lên trước nút Đóng + remove nút Đóng; (2) velocity note không cập nhật realtime khi thay đổi ccHeight (độ rộng) hoặc showCC (bật/tắt thanh velocity).
- **FIX (app.jsx PianoRollTabEditor):**
(1) Xóa nút "Đóng" (onClose) — chèn div ml-auto [Lưu + Export MIDI] (di chuyển từ cuối toolbar — xóa bản cũ); sửa cân bằng ngoặc (bản cũ `"Export MIDI")))` đóng cả toolbar div — thêm `)` tại `"Norm")))`).
(2) Effect vẽ CC canvas — thêm `ccHeight, showCC` vào deps (`[notes, ccMode, rollZoom, viewWidth, selectedNoteIds, renderBeatOffset, ccHeight, showCC]`) — đổi độ rộng/bật tắt → vẽ lại realtime.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070845), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL toolbar: không còn nút Đóng; Lưu/Export ở cuối (trước hàng tool Scale/Chord/Vel); kéo đổi độ rộng thanh velocity / bấm Vel toggle → velocity bars cập nhật ngay.
### [2026-08-07 08:50] Task: FIX TDZ — ccHeight/showCC khai báo SAU effect vẽ CC
- **Lỗi user:** `Uncaught ReferenceError: Cannot access 'ccHeight' before initialization`.
- **Nguyên nhân:** 08:45 thêm ccHeight/showCC vào deps useLayoutEffect vẽ CC (7620) — NHƯNG 2 state khai báo ở 8542 (SAU effect) → deps array tham chiếu biến trước khai báo → TDZ error khi render.
- **FIX (app.jsx):** di chuyển `const [showCC, setShowCC]` + `const [ccHeight, setCcHeight]` LÊN TRƯỚC useLayoutEffect vẽ CC (7619) — xóa bản trùng cũ (8542).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070850), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → mở PIANO ROLL → KHÔNG còn lỗi TDZ; đổi độ rộng thanh velocity / toggle Vel → vẽ lại realtime.
### [2026-08-07 08:55] Task: Tăng size tool từ "Snap to Scale" đến cuối hàng
- **Yêu cầu user:** tăng size các tool từ Snap to Scale đến cuối hàng (Snap to Scale toggle, Scale ▾, Root ▾, Force, Chord ▾, Stamp, +3/+5/+7, Vel: Comp/Norm).
- **FIX (app.jsx):** trong khối (Snap to Scale → Norm) — `text-[10px]``text-xs` (12px) — 9 chỗ (Force/Stamp/+3/+5/+7/Comp/Norm buttons + Chord select...).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070855), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL toolbar → các tool cuối hàng to hơn (text-xs).
### [2026-08-07 09:00] Task: Đồng bộ font hàng tool (Snap to Scale → Norm) lên text-sm
- **Yêu cầu user:** tăng kích thước font chữ trong hàng đó cho ĐỒNG BỘ.
- **FIX (app.jsx):** khối Snap to Scale → Norm — `text-xs``text-sm` (12 chỗ: 5 container div + buttons + Chord select + nhãn) + thêm `text-sm` vào 2 select Scale/Root (mặc định browser) — toàn hàng đồng bộ 14px.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070900), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL toolbar hàng tool — font đồng bộ text-sm (14px).
### [2026-08-07 09:05] Task: Tăng chiều rộng input target velocity (Vel Comp)
- **Yêu cầu user:** tăng chiều rộng scrollbox/input của Vel Comp để chứa đủ số velocity (127).
- **FIX (app.jsx):** input `velocityTarget` (bên cạnh nút Comp) — `w-9``w-14` (đủ 3 chữ số 0-127).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070905), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL → input target (cạnh Comp) rộng hơn, hiện đủ "127".
### [2026-08-07 09:10] Task: Ctrl+C/X trong PIANO ROLL — CHỈ copy/cắt notes được chọn
- **Yêu cầu user:** Ctrl-X/Ctrl-C trong PIANO ROLL TAB chỉ cắt/copy các NOTES ĐƯỢC CHỌN.
- **FIX (app.jsx keydown 7106):** Ctrl+C — copy `notes.filter(selectedNoteIds)` (toast số nốt chọn; không chọn → toast "Chọn notes trước khi copy"); Ctrl+X — copy + xóa notes chọn (filter bỏ selectedNoteIds); Ctrl+V giữ nguyên (paste clipboard).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070910), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — chọn 1 số notes → Ctrl+C → Ctrl+V paste đúng notes chọn; Ctrl+X chỉ cắt notes chọn; không chọn → toast nhắc.
### [2026-08-07 09:15] Task: Ctrl+drag marquee chọn notes → Ctrl+X báo "Chọn notes trước khi cắt"
- **Báo cáo user:** Ctrl+drag marquee chọn notes → Ctrl+X → lỗi "Chọn notes trước khi cắt".
- **Nguyên nhân:** marquee mousemove (8018-8033) ĐÃ chọn notes live (setSelectedNoteIds) — NHƯNG keydown handler (Ctrl+C/X — 7106) deps thiếu `selectedNoteIds` → closure STALE (selectedNoteIds rỗng từ render trước) → filter = [] → toast lỗi.
- **FIX (app.jsx):** (a) thêm `selectedNoteIds` vào deps effect keydown 7106; (b) bổ sung — handleGridMouseUp khi kết thúc marquee → chọn lại notes trong vùng (đảm bảo dù không qua mousemove lần cuối).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070915), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — Ctrl+drag marquee (notes highlight) → Ctrl+X → cắt đúng notes trong vùng; Ctrl+C → copy.
### [2026-08-07 09:20] Task: Ctrl+V trong PIANO ROLL hiện prompt "Nhập Gain điều chỉnh (dB)"
- **Báo cáo user:** nhấn Ctrl-V trong PIANO ROLL TAB → browser prompt "Nhập Gain điều chỉnh (dB):" — cần remove trong piano roll tab.
- **Nguyên nhân:** sub-tab keydown handler (16590) bắt `e.key === 'v'` KHÔNG kiểm tra Ctrl → Ctrl+V cũng trúng → prompt gain (fade gain shortcut 'v' đơn).
- **FIX (app.jsx):** thêm `!ctrl && !e.metaKey && !e.altKey` — chỉ 'v' đơn (không modifier) mới mở prompt; Ctrl+V → chạy paste notes piano roll (handler 7106).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070920), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — Ctrl+V → paste notes (KHÔNG prompt); nhấn 'v' đơn → prompt gain như cũ (sub-tab audio).
### [2026-08-07 09:25] Task: Ctrl+V dán notes tại vị trí con trỏ playhead
- **Yêu cầu user:** Ctrl-V dán các notes đã cut/copy bắt đầu ở vị trí con trỏ playhead đang đứng.
- **FIX (app.jsx keydown 7106 — Ctrl+V):** `pasteBeat = st.currentTime / (60/bpm)` (playhead → beat) — `offset = pasteBeat - minStart(clipboard)` — dán với `start_beat = n.start_beat + offset` (note đầu tiên nằm đúng playhead; clamp ≥ 0).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070925), `wiki.md`. Rebuild precompiled (build PASS — pasteBeat ×2).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — di chuyển playhead → Ctrl+V → notes dán bắt đầu đúng vị trí playhead.
### [2026-08-07 09:30] Task: PIANO ROLL — Shift+click toggle chọn; Ctrl+click+drag COPY nhanh; drag move pitch
- **Yêu cầu user:** (1) Ctrl+Click → Shift+Click để thêm/bớt note vào nhóm chọn; (2) Ctrl+Click giữ + drag → copy nhanh nhóm notes/note đến vị trí mới (cùng pitch — drag đổi vị trí/pitch); (3) drag note/nhóm → vị trí + pitch khác.
- **Trạng thái:** (3) ĐÃ CÓ (7895-7922 — mode 'move' — deltaBeat + deltaPitch).
- **FIX (app.jsx PianoRollTabEditor handleGridMouseDown):**
(1) toggle selection: `e.ctrlKey``e.shiftKey` (Shift+click thêm/bớt note).
(2) Ctrl+click TRÊN NOTE → clone notes (nhóm chọn nếu note trong nhóm, ngược lại note đơn) + setDraggedNote mode 'move' (selectedNotesOffset + clickedOriginalStartBeat) → kéo clones đến vị trí/pitch mới (gốc giữ) — mousemove move có sẵn.
(3) Ctrl+click empty → marquee (giữ); Ctrl+Shift+click → split (giữ).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070930), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — Shift+click thêm/bớt note chọn; Ctrl+giữ+click note → kéo → copy nhóm đến vị trí mới; drag note (không modifier) → move vị trí + pitch.
### [2026-08-07 09:35] Task: FIX Ctrl+click+drag copy — notes không bám vị trí con trỏ
- **Báo cáo user:** ctrl-click-drag — notes không được copy ngay tại vị trí con trỏ chuột.
- **Nguyên nhân:** copy-drag set `startOffsetBeat: beat` (vị trí chuột) — move mousemove tính `deltaBeat = snap(beatMouse - startOffsetBeat) - refOrigStart` → delta bị lệch (Δ=0 → -noteStart → clones nhảy về beat 0 / không bám chuột). Move thường dùng `startOffsetBeat = beat - clickedNote.start_beat` (offset trong note).
- **FIX (app.jsx):** copy-drag → `startOffsetBeat: beat - clickedNote.start_beat` (giống move thường) → kéo → delta = snap(Δ) → clones bám chuột (vị trí + pitch).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070935), `wiki.md`. Rebuild precompiled (build PASS — 2 chỗ đồng bộ).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — Ctrl+click note → kéo → các bản copy theo đúng vị trí con trỏ (beat + pitch).
### [2026-08-07 09:40] Task: PIANO ROLL — (1) nút track column: active ghost → main + MUTE (2) follow playhead (3) status hint động
- **Yêu cầu user:**
(1) Cột trái (nút tên track): click → active ghost notes của track đó thành MAIN notes để chỉnh sửa; cuối nút thêm nút MUTE (mặc định MUTE — unmute → play ghost cùng main notes — chức năng tương tự nút toggle cũ).
(2) Khi play → playhead di chuyển GIỮA piano roll, notes trôi sang trái; stop → playhead về ĐẦU (luôn hiển thị trong view).
(3) Status bar gợi ý: mouse trong piano roll → "Scroll: Up/Down | Drag: Draw notes"; giữ Shift → "Shift+Click: select/unselect..."; giữ Ctrl → "Ctrl+drag: fast copy notes...".
- **FIX (app.jsx):**
(1) Track column (9270): nút tên track → `handleSwitchMidiItem(m.id)` (mở item — chỉnh sửa main notes); thêm nút MUTE (w-6 — mặc định 'M' zinc, unmute '♪' green — toggle activePlayTrackIds + onRealtimePlay).
(2) Effect follow: `[isPlaying, st.currentTime]` — play → `scrollLeft = phBeat*ppb - clientWidth/2` (playhead giữa, notes trôi trái); stop → `scrollLeft = 0` (về đầu).
(3) App state `prHint` + `window.__setPrHint`; piano roll: `prKeyStateRef`/`prMouseInRef` + keydown/keyup + container onMouseEnter/Leave → set hint theo Ctrl/Shift; status bar hiển thị `prHint` (cyan) thay "Scroll: Zoom".
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070940), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — click tên track khác → mở item đó (sửa được); nút M — click → ♪ xanh (play cùng main); play → playhead giữa + trôi trái; stop → về đầu; hover trong piano roll → status bar đổi gợi ý theo Shift/Ctrl.
### [2026-08-07 09:45] Task: (1) Cân bằng tên track cột trái (2) Xóa nhãn "Ctrl+Scroll: Playhead" cố định
- **Yêu cầu user:** (1) sửa chiều cao nút tên track cột trái — tên nút cân bằng giữa nút; (2) xóa nhãn cố định "Ctrl+Scroll: Playhead" cuối status bar — để gợi ý adaptive hiển thị đủ nội dung.
- **FIX (app.jsx):**
(1) Nút tên track: `h-[20px]``h-[26px]` + `flex items-center justify-center` + text `text-[13px] leading-none`; nút MUTE đồng bộ `h-[26px]` + flex center.
(2) Status bar: xóa span "|" + span "Ctrl+Scroll: Playhead" (icon keyboard) — còn `prHint` (cyan) hoặc "Scroll: Zoom" — gợi ý adaptive có đủ chỗ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070945), `wiki.md`. Rebuild precompiled (build PASS — Ctrl+Scroll: Playhead = 0).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — tên track nằm cân bằng giữa nút (26px); status bar cuối không còn "Ctrl+Scroll: Playhead" — gợi ý adaptive hiển thị đầy đủ.
### [2026-08-07 09:50] Task: FIX Follow playhead chưa hoạt động — dùng sai isPlaying (main)
- **Báo cáo user:** follow playhead (0940) vẫn chưa thực hiện được.
- **Nguyên nhân:** effect dùng prop `isPlaying` = MAIN play — piano roll play chỉ set `st.isPlaying` (main vẫn false — handlePlayPause sub-tab stopAllPlayback) → effect luôn rơi nhánh "stop" → scroll về đầu mãi.
- **FIX (app.jsx effect follow):** `const playing = isPlaying || !!st.isPlaying` — follow khi piano roll play LẪN main play; deps thêm `st.isPlaying`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070950), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL → play (tab) → playhead ở giữa + notes trôi trái; stop → về đầu.