22 Commits

Author SHA1 Message Date
3dtours 9099529403 FIX: engine.spec dung os thay _os (NameError luc build) 2026-08-08 15:56:34 +00:00
3dtours 3795ea9d78 FIX: scipy hiddenimports scan FILESYSTEM (glob .py/.pyd/.so) thay collect_submodules - walk_packages bo qua am tham subpackage import loi luc build (mat ca cay scipy.sparse.csgraph._shortest_path tren may user) 2026-08-08 15:55:08 +00:00
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
29 changed files with 1201 additions and 35 deletions
+3
View File
@@ -29,3 +29,6 @@ samples/
*.log
celerybeat-schedule
node_modules
src-tauri/target/
src-tauri/binaries/
src-tauri/vc_redist.x64.exe
+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`
+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")
+21 -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
+270 -9
View File
@@ -6469,6 +6469,155 @@ const SystemManagerModal = ({
}, "Xóa")))))))));
};
/*
ABOUT / HELP / PREFERENCES MODALS (menu Help + Tools Preferences)
*/
const APP_VERSION = '1.0.0'; // khp src-tauri/tauri.conf.json
const AboutModal = ({ isOpen, onClose }) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm" onClick={onClose}>
<div className="bg-[#1e1e24] border border-zinc-700 rounded-xl shadow-2xl w-full max-w-md p-6 text-slate-200" onClick={e => e.stopPropagation()}>
<div className="flex items-center gap-3 mb-4">
<img src="/favicon.svg" alt="SonicForge Studio" className="w-12 h-12 rounded-lg bg-zinc-900 border border-zinc-700 shadow-lg object-contain p-0.5" />
<div>
<h2 className="text-lg font-bold text-white">SonicForge Studio</h2>
<p className="text-xs text-zinc-400">Professional DAW v{APP_VERSION}</p>
</div>
</div>
<div className="space-y-2 text-sm">
<p className="text-zinc-300 leading-relaxed">Phần mềm sản xuất âm nhạc (DAW) soạn nhạc, ghi âm, chỉnh sửa MIDI/Audio, SoundFont &amp; VST, trộn master.</p>
<div className="pt-2 border-t border-zinc-800 space-y-1 text-xs">
<div className="flex items-center gap-2"><span className="text-zinc-500 w-20 shrink-0">Developer</span><span className="text-cyan-400 font-semibold">Lộc Phạm</span></div>
<div className="flex items-center gap-2"><span className="text-zinc-500 w-20 shrink-0">Email</span><a href="mailto:tranloclqd@gmail.com" className="text-cyan-400 hover:underline">tranloclqd@gmail.com</a></div>
<div className="flex items-center gap-2"><span className="text-zinc-500 w-20 shrink-0">Version</span><span className="text-zinc-300">{APP_VERSION}</span></div>
<div className="flex items-center gap-2"><span className="text-zinc-500 w-20 shrink-0">Build</span><span className="text-zinc-300">Standalone (Tauri v2 + PyInstaller)</span></div>
</div>
</div>
<div className="mt-5 flex justify-end">
<button onClick={onClose} className="px-4 py-1.5 bg-cyan-700 hover:bg-cyan-600 rounded text-xs font-bold text-white transition">Đóng</button>
</div>
</div>
</div>
);
};
const HelpModal = ({ isOpen, onClose, lang }) => {
if (!isOpen) return null;
const vi = lang !== 'en';
const sections = vi ? [
{ title: '🚀 Bắt đầu nhanh', body: 'Nhấn phím Cách (Space) để Play/Pause. Nhấn phím / để dừng. Dùng nút Record (●) để ghi âm/MIDI. Tạo track mới từ menu Track hoặc nút "+ Add Track".' },
{ title: '🎹 MIDI & ARM', body: 'Bật nút ARM đỏ trên track để nhận phím từ MIDI keyboard. Vào menu Tools → MIDI Devices để chọn thiết bị. Khi ARM + bấm phím, âm preview phát và VU meter nhảy theo trường độ.' },
{ title: '🎼 Piano Roll', body: 'Nhấp đúp vào MIDI item để mở Piano Roll. Dùng Ctrl+scroll hoặc nút +/ để zoom. Nhấp để vẽ note, kéo để di chuyển, Ctrl+kéo để copy. Phím tắt: C (vẽ), E (tẩy), Space (nghe).' },
{ title: '🎛️ FX & Master', body: 'Chọn track → nút FX để mở FX Rack (thêm EQ, compressor...). Bấm nút PWR ở Master để mở Mastering Panel: EQ 4 băng, compressor, limiter. Mọi thay đổi áp dụng realtime.' },
{ title: '🧪 SoundFont & VST', body: 'Vào Tools → Plugin Manager để quét SoundFont (.sf2/.sf3) và VSTi. Track MIDI dùng SoundFont làm nhạc cụ — chọn instrument từ nút Synth trên track.' },
{ title: '🤖 AI', body: 'Tools → Config AI Providers để cấu hình API (OpenAI, Gemini, Ollama...). Dùng AI Prompt Generator (nút ✨) để sinh MIDI/ý tưởng; AI MIDI Preset Manager để lưu preset.' },
{ title: '💾 Lưu & Xuất', body: 'Ctrl+S lưu project (cloud/temp), Ctrl+Shift+S Save As. Export WAV qua nút Export — chọn vùng, format, bitrate rồi Render.' },
{ title: '⌨️ Phím tắt', body: 'Space: Play/Pause · /: Stop · Ctrl+Z/Y: Undo/Redo · Ctrl+C/X/V: Copy/Cut/Paste · Ctrl+S: Save · Ctrl+N: New · S: Split · Ctrl+E: Edit in new tab · Ctrl+wheel: zoom timeline' },
] : [
{ title: '🚀 Quick start', body: 'Press Space to Play/Pause. Press / to stop. Use Record (●) to capture audio/MIDI. Add tracks from the Track menu or the "+ Add Track" button.' },
{ title: '🎹 MIDI & ARM', body: 'Enable the red ARM button on a track to receive keys from a MIDI keyboard. Go to Tools → MIDI Devices to pick your device. When ARM + key press, preview audio plays and the VU meter animates for the note length.' },
{ title: '🎼 Piano Roll', body: 'Double-click a MIDI item to open the Piano Roll. Use Ctrl+scroll or +/ buttons to zoom. Click to draw notes, drag to move, Ctrl+drag to copy. Shortcuts: C (draw), E (erase), Space (listen).' },
{ title: '🎛️ FX & Master', body: 'Select a track → FX button to open the FX Rack (EQ, compressor...). Press the PWR button on the Master to open the Mastering Panel: 4-band EQ, compressor, limiter — all realtime.' },
{ title: '🧪 SoundFont & VST', body: 'Tools → Plugin Manager scans SoundFonts (.sf2/.sf3) and VSTi. MIDI tracks use SoundFonts as instruments — pick one from the Synth button on the track.' },
{ title: '🤖 AI', body: 'Tools → Config AI Providers to set up APIs (OpenAI, Gemini, Ollama...). Use the AI Prompt Generator (✨) to create MIDI/ideas; AI MIDI Preset Manager stores presets.' },
{ title: '💾 Save & Export', body: 'Ctrl+S saves the project (cloud/temp), Ctrl+Shift+S Save As. Export WAV via the Export button — pick range, format, bitrate, then Render.' },
{ title: '⌨️ Shortcuts', body: 'Space: Play/Pause · /: Stop · Ctrl+Z/Y: Undo/Redo · Ctrl+C/X/V: Copy/Cut/Paste · Ctrl+S: Save · Ctrl+N: New · S: Split · Ctrl+E: Edit in new tab · Ctrl+wheel: zoom timeline' },
];
return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm" onClick={onClose}>
<div className="bg-[#1e1e24] border border-zinc-700 rounded-xl shadow-2xl w-full max-w-2xl max-h-[85vh] flex flex-col text-slate-200" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between px-5 py-3 border-b border-zinc-800 shrink-0">
<h2 className="text-base font-bold text-white">{vi ? 'Hướng dẫn sử dụng SonicForge Studio' : 'SonicForge Studio User Guide'}</h2>
<button onClick={onClose} className="w-6 h-6 rounded hover:bg-zinc-700 text-zinc-400 hover:text-white text-sm"></button>
</div>
<div className="overflow-y-auto px-5 py-4 space-y-3">
{sections.map((s, i) => (
<div key={i} className="bg-zinc-900/60 border border-zinc-800 rounded-lg p-3">
<h3 className="text-sm font-bold text-cyan-400 mb-1">{s.title}</h3>
<p className="text-xs text-zinc-300 leading-relaxed">{s.body}</p>
</div>
))}
</div>
<div className="px-5 py-3 border-t border-zinc-800 flex justify-end shrink-0">
<button onClick={onClose} className="px-4 py-1.5 bg-cyan-700 hover:bg-cyan-600 rounded text-xs font-bold text-white transition">{vi ? 'Đóng' : 'Close'}</button>
</div>
</div>
</div>
);
};
const PreferencesModal = ({ isOpen, onClose, prefs, onPrefsChange }) => {
if (!isOpen) return null;
const vi = prefs.language !== 'en';
const themes = [
{ id: 'dark', name: vi ? 'Tối (mặc định)' : 'Dark (default)', swatch: 'from-zinc-700 to-zinc-900', ring: 'ring-cyan-400' },
{ id: 'midnight', name: vi ? 'Đêm xanh' : 'Midnight', swatch: 'from-sky-800 to-slate-950', ring: 'ring-sky-400' },
{ id: 'forest', name: vi ? 'Rừng xanh' : 'Forest', swatch: 'from-emerald-700 to-green-950', ring: 'ring-emerald-400' },
{ id: 'violet', name: vi ? 'Tím' : 'Violet', swatch: 'from-violet-700 to-purple-950', ring: 'ring-violet-400' },
{ id: 'graphite', name: vi ? 'Than chì' : 'Graphite', swatch: 'from-zinc-600 to-neutral-900', ring: 'ring-zinc-300' },
];
const set = (k, v) => onPrefsChange({ ...prefs, [k]: v });
return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm" onClick={onClose}>
<div className="bg-[#1e1e24] border border-zinc-700 rounded-xl shadow-2xl w-full max-w-md max-h-[85vh] overflow-y-auto p-5 text-slate-200" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between mb-4">
<h2 className="text-base font-bold text-white">{vi ? 'Tùy chọn (Preferences)' : 'Preferences'}</h2>
<button onClick={onClose} className="w-6 h-6 rounded hover:bg-zinc-700 text-zinc-400 hover:text-white text-sm"></button>
</div>
{/* Theme */}
<div className="mb-5">
<h3 className="text-xs font-bold text-zinc-400 uppercase tracking-wider mb-2">{vi ? '🎨 Chủ đề màu' : '🎨 Theme'}</h3>
<div className="grid grid-cols-1 gap-1.5">
{themes.map(th => (
<button key={th.id} onClick={() => set('theme', th.id)}
className={`flex items-center gap-2.5 px-2.5 py-2 rounded-lg border text-xs text-left transition ${prefs.theme === th.id ? 'border-cyan-500 bg-zinc-800' : 'border-zinc-700 bg-zinc-900 hover:bg-zinc-800'}`}>
<span className={`w-6 h-6 rounded-md bg-gradient-to-br ${th.swatch} ring-1 ring-black/40 shrink-0 ${prefs.theme === th.id ? `ring-2 ${th.ring}` : ''}`}></span>
<span className="text-zinc-200">{th.name}</span>
{prefs.theme === th.id && <span className="ml-auto text-cyan-400 text-[10px] font-bold"></span>}
</button>
))}
</div>
</div>
{/* Language */}
<div className="mb-5">
<h3 className="text-xs font-bold text-zinc-400 uppercase tracking-wider mb-2">{vi ? '🌐 Ngôn ngữ' : '🌐 Language'}</h3>
<div className="flex gap-1.5">
{[{ id: 'vi', label: 'Tiếng Việt' }, { id: 'en', label: 'English' }].map(l => (
<button key={l.id} onClick={() => set('language', l.id)}
className={`flex-1 px-2 py-1.5 rounded-lg border text-xs font-semibold transition ${prefs.language === l.id ? 'border-cyan-500 bg-cyan-900/40 text-cyan-300' : 'border-zinc-700 bg-zinc-900 text-zinc-400 hover:bg-zinc-800'}`}>
{l.label}
</button>
))}
</div>
<p className="text-[10px] text-zinc-500 mt-1">{vi ? 'Áp dụng ngay cho menu & hướng dẫn.' : 'Applies immediately to menus & guide.'}</p>
</div>
{/* Button font size */}
<div className="mb-5">
<h3 className="text-xs font-bold text-zinc-400 uppercase tracking-wider mb-2">{vi ? '🔠 Cỡ chữ nút bấm' : '🔠 Button font size'}</h3>
<div className="flex gap-1.5">
{[{ id: 'sm', label: vi ? 'Nhỏ' : 'Small' }, { id: 'md', label: vi ? 'Vừa' : 'Medium' }, { id: 'lg', label: vi ? 'Lớn' : 'Large' }].map(f => (
<button key={f.id} onClick={() => set('buttonFontSize', f.id)}
className={`flex-1 px-2 py-1.5 rounded-lg border text-xs font-semibold transition ${prefs.buttonFontSize === f.id ? 'border-cyan-500 bg-cyan-900/40 text-cyan-300' : 'border-zinc-700 bg-zinc-900 text-zinc-400 hover:bg-zinc-800'}`}>
{f.label}
</button>
))}
</div>
</div>
<div className="flex justify-end gap-2 pt-2 border-t border-zinc-800">
<button onClick={onClose} className="px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 rounded text-xs font-bold text-white transition">{vi ? 'Hủy' : 'Cancel'}</button>
<button onClick={onClose} className="px-4 py-1.5 bg-cyan-700 hover:bg-cyan-600 rounded text-xs font-bold text-white transition">{vi ? 'Lưu & Đóng' : 'Save & Close'}</button>
</div>
</div>
</div>
);
};
const AIPresetModal = ({ isOpen, onClose }) => {
if (!isOpen) return null;
@@ -14500,6 +14649,11 @@ const App = () => {
if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity(as.trackId, scaledVel);
}
// Gi VU theo trưng đ: tăng counter note đang gi
// (tick gi peak cho ti khi note-off)
try {
heldMidiNotesRef.current[as.trackId] = (heldMidiNotesRef.current[as.trackId] || 0) + 1;
} catch (err) { }
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, asProg, null, asCh, asSe);
});
}
@@ -14512,6 +14666,10 @@ const App = () => {
if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity(at.id, scaledVel);
}
// Gi VU theo trưng đ: tăng counter note đang gi
try {
heldMidiNotesRef.current[at.id] = (heldMidiNotesRef.current[at.id] || 0) + 1;
} catch (err) { }
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, atProg, atDest, atCh, atSe);
});
}
@@ -14535,6 +14693,16 @@ const App = () => {
if (!st.synth_engine && st.midiChannel === undefined) return;
var stCh = assignTrackMidiChannel(st, stopTracks);
window.SonicSF.stopNote(stCh, pitch);
// Gim counter note đang gi hết note VU đưc phép decay/tt
// (tick s thy counter = 0 và không còn gi peak na)
try {
if (heldMidiNotesRef.current[st.id] !== undefined) {
heldMidiNotesRef.current[st.id] = Math.max(0, heldMidiNotesRef.current[st.id] - 1);
if (heldMidiNotesRef.current[st.id] === 0) {
delete heldMidiNotesRef.current[st.id];
}
}
} catch (err) { }
});
}
}
@@ -15494,6 +15662,11 @@ const App = () => {
}, [isPlaying]);
const midiVuActivityRef = useRef({});
// Đếm s note MIDI đang GI per-track (ARM + MIDI keyboard live input).
// VU tick dùng ref này: còn note gi gi peak (không decay) VU animate
// ĐÚNG trưng đ âm thanh (user bug: nhn phím gi âm còn kêu nhưng VU tt
// sau ~0.5s vì decay 0.75/frame). Hết note (note-off) mi decay/tt.
const heldMidiNotesRef = useRef({});
const triggerMidiVuActivity = (trackId, velocity) => {
if (!trackId) return;
const velFactor = typeof velocity === 'number' ? (velocity > 1 ? velocity / 127 : velocity) : 0.8;
@@ -15538,6 +15711,51 @@ const App = () => {
const [aiPresetModalOpen, setAiPresetModalOpen] = useState(false);
const [aiPresetVersion, setAiPresetVersion] = useState(0);
const [showMasteringModal, setShowMasteringModal] = useState(false);
// Help / About / Preferences (menu Help + Tools)
const [aboutModalOpen, setAboutModalOpen] = useState(false);
const [helpModalOpen, setHelpModalOpen] = useState(false);
const [preferencesModalOpen, setPreferencesModalOpen] = useState(false);
const [prefs, setPrefs] = useState(() => {
try {
const saved = localStorage.getItem('sf_prefs');
if (saved) return JSON.parse(saved);
} catch (e) { }
return { theme: 'dark', language: 'vi', buttonFontSize: 'md' };
});
// Áp dng theme + font size lên <html> ngay khi đi
useEffect(() => {
try {
document.documentElement.setAttribute('data-theme', prefs.theme || 'dark');
document.documentElement.setAttribute('data-btnfont', prefs.buttonFontSize || 'md');
localStorage.setItem('sf_prefs', JSON.stringify(prefs));
} catch (e) { }
}, [prefs]);
// Load preferences t server (nếu có tài khon) khi khi đng
useEffect(() => {
let cancelled = false;
if (window.SonicAPI && window.SonicAPI.getPreferences) {
window.SonicAPI.getPreferences().then(res => {
if (cancelled || !res || !res.success || !res.preferences) return;
const p = res.preferences;
if (p.theme || p.language || p.buttonFontSize) {
setPrefs(prev => ({
theme: p.theme || prev.theme,
language: p.language || prev.language,
buttonFontSize: p.buttonFontSize || prev.buttonFontSize
}));
}
}).catch(() => { });
}
return () => { cancelled = true; };
}, []);
// Lưu preferences lên server khi đi
const handlePrefsChange = (next) => {
setPrefs(next);
try { localStorage.setItem('sf_prefs', JSON.stringify(next)); } catch (e) { }
if (window.SonicAPI && window.SonicAPI.savePreferences) {
window.SonicAPI.savePreferences(next).catch(() => { });
}
};
// Unified FX Rack target context (unified_fx_rack_panel.md): { trackId, trackName } | null
const [fxRackTarget, setFxRackTarget] = useState(null);
window.__openFxRack = (trackId, trackName) => setFxRackTarget({ trackId, trackName });
@@ -20595,6 +20813,9 @@ const App = () => {
// âm cũ user 06:45: tt âm MAIN items khi vào SECTION-TAB VU main
// items phi tt theo, không "din" tiếp).
try { midiVuActivityRef.current = {}; } catch (e) { }
// Clear luôn counter note đang gi nếu không, sau stop (âm đã dng)
// tick vn thy heldCnt > 0 gi peak VU dính mãi (user bug 08:08).
try { heldMidiNotesRef.current = {}; } catch (e) { }
stopMidiCapture();
if (window.SonicSF) {
try { window.SonicSF.stopAll(); } catch (e) { console.warn('[Stop] stopAll error:', e); }
@@ -25744,8 +25965,18 @@ STRICT CONSTRAINTS:
try {
// Khi STOP (không play, không recording): v VU RNG không đc analyser/
// activity hết animation dính + master không full sau khi dng play.
// NGOI L: nếu có MIDI activity live (bt ARM track + bm phím MIDI
// keyboard KHÔNG play/record) thì KHÔNG early-return track VU phi
// nhy theo midiVuActivityRef + decay (user bug: ARM + phím MIDI VU
// đng yên vì block này v rng và return trưc khi đc activity).
const isRecActive = activeAudioRecordersRef.current && Object.keys(activeAudioRecordersRef.current).length > 0;
if (!isPlaying && !isRecActive) {
let liveMidiVu = false;
try {
for (const _k in midiVuActivityRef.current) {
if (midiVuActivityRef.current[_k] > 0.001) { liveMidiVu = true; break; }
}
} catch (e) { }
if (!isPlaying && !isRecActive && !liveMidiVu) {
setMasterVU(0);
setMasterMeterPeak(0);
Object.keys(trackVuRefs.current).forEach(k => {
@@ -25846,12 +26077,18 @@ STRICT CONSTRAINTS:
// velocity thp (âm SF nh < 0.03) VU không nhy (user bug 07:10).
let midiPeak = isAudible ? (midiVuActivityRef.current[vuKey] || 0) : 0;
if (midiPeak > 0) {
// Decay 0.75 (~0.5s) cân bng: note đơn/velocity thp hin th rõ
// NHƯNG tt nhanh sau hết note (decay 0.85 ~1s quá lâu user:
// "vn din animation" sau khi âm hết bug 07:15).
midiVuActivityRef.current[vuKey] = midiPeak * 0.75;
if (midiVuActivityRef.current[vuKey] < 0.01) {
midiVuActivityRef.current[vuKey] = 0;
// ARM + MIDI keyboard: còn note đang GI gi nguyên peak, KHÔNG
// decay VU animate đúng trưng đ âm thanh (user bug 08:08: âm còn
// kêu nhưng VU tt sau ~0.5s vì decay 0.75/frame). Hết note decay
// 0.75 (~0.5s) tt nhanh như trưc.
const heldCnt = heldMidiNotesRef.current[vuKey] || 0;
if (heldCnt > 0) {
// Gi nguyên peak trong khi âm đang kêu (không decay)
} else {
midiVuActivityRef.current[vuKey] = midiPeak * 0.75;
if (midiVuActivityRef.current[vuKey] < 0.01) {
midiVuActivityRef.current[vuKey] = 0;
}
}
}
// KHÔNG gp sfAudioPeak vào audioPeak: SF là 1 output CHUNG cho mi
@@ -26239,13 +26476,25 @@ STRICT CONSTRAINTS:
setPluginManagerModalOpen(true);
window.SonicAPI.listPlugins().then(data => setPluginsData(data)).catch(() => {});
}
}, {
sep: true
}, {
label: 'Preferences...',
icon: 'settings-2',
action: () => setPreferencesModalOpen(true)
}]
}, {
label: 'Help',
items: [{
label: 'About SonicForge',
label: 'Hướng dẫn sử dụng...',
icon: 'book-open',
action: () => setHelpModalOpen(true)
}, {
sep: true
}, {
label: 'About SonicForge Studio...',
icon: 'info',
action: () => showToast('SonicForge Studio v1.0 - Professional DAW', 'info')
action: () => setAboutModalOpen(true)
}]
}].map(menu => /*#__PURE__*/React.createElement("div", {
key: menu.label,
@@ -29221,6 +29470,18 @@ STRICT CONSTRAINTS:
const active = providers.find(p => p.is_active) || providers[0];
if (active) setSelectedProviderId(active.id);
}
}), /*#__PURE__*/React.createElement(AboutModal, {
isOpen: aboutModalOpen,
onClose: () => setAboutModalOpen(false)
}), /*#__PURE__*/React.createElement(HelpModal, {
isOpen: helpModalOpen,
onClose: () => setHelpModalOpen(false),
lang: prefs.language
}), /*#__PURE__*/React.createElement(PreferencesModal, {
isOpen: preferencesModalOpen,
onClose: () => setPreferencesModalOpen(false),
prefs: prefs,
onPrefsChange: handlePrefsChange
}), /*#__PURE__*/React.createElement(SystemManagerModal, {
isOpen: systemManagerModalOpen,
onClose: () => setSystemManagerModalOpen(false)
File diff suppressed because one or more lines are too long
+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": {
+56 -2
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>
@@ -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=202608070950" 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()
+128
View File
@@ -0,0 +1,128 @@
# 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. Giai phap TRIET DE:
# scan FILESYSTEM toan bo site-packages/scipy (khong import, khong walk —
# pkgutil.walk_packages BO QUA AM THAM subpackage import loi luc build,
# da gap: may user mat ca cay scipy.sparse.csgraph._shortest_path).
# Bat moi module .py + C-extension .pyd/.so -> hiddenimports day du.
import importlib.util as _ilu
import glob as _glob
_scipy_spec = _ilu.find_spec('scipy')
_scipy_dir = os.path.dirname(os.path.abspath(_scipy_spec.origin))
_scipy_hidden = []
for _ext in ('*.py', '*.pyd', '*.so'):
for _f in _glob.glob(os.path.join(_scipy_dir, '**', _ext), recursive=True):
_rel = os.path.relpath(_f, _scipy_dir)
_base = os.path.basename(_rel).split('.')[0] # bo .cpython-312-x86_64... .so
_pkg = os.path.dirname(_rel).replace(os.sep, '.')
_mod = ('scipy.' + _pkg + '.' + _base) if _pkg else ('scipy.' + _base)
if _mod not in _scipy_hidden:
_scipy_hidden.append(_mod)
# 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')
# scipy.sparse.csgraph cung lazy-import C-extension tu ben trong ham (vd
# _shortest_path, _traversal, _matching) — hiddenimport tinh phong walk miss.
for _m in ('scipy.sparse.csgraph._shortest_path', 'scipy.sparse.csgraph._traversal',
'scipy.sparse.csgraph._matching', 'scipy.sparse.csgraph._min_spanning_tree'):
if _m not in _scipy_hidden:
_scipy_hidden.append(_m)
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())
+106
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)