Compare commits
11 Commits
main
...
standalone
| Author | SHA1 | Date | |
|---|---|---|---|
| 3618e3c591 | |||
| b490aa9951 | |||
| be93f9f55a | |||
| 7dda81b1d3 | |||
| f7b0172fdc | |||
| 9bebbe8e7a | |||
| 4e4391b315 | |||
| e978abf0e9 | |||
| bb699631bf | |||
| 0a1f3efd64 | |||
| 9ef622f29a |
@@ -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/
|
||||||
@@ -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
@@ -28,4 +28,7 @@ samples/
|
|||||||
.vscode/
|
.vscode/
|
||||||
*.log
|
*.log
|
||||||
celerybeat-schedule
|
celerybeat-schedule
|
||||||
node_modules
|
node_modules
|
||||||
|
src-tauri/target/
|
||||||
|
src-tauri/binaries/
|
||||||
|
src-tauri/vc_redist.x64.exe
|
||||||
@@ -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** và **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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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 8000–8010 + 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`
|
||||||
@@ -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).
|
||||||
+19
-3
@@ -1,15 +1,31 @@
|
|||||||
import os
|
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:
|
class Settings:
|
||||||
REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
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_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")
|
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 = _app_dir()
|
||||||
APP_DIR: str = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
BASE_DIR: str = os.path.dirname(APP_DIR)
|
BASE_DIR: str = os.path.dirname(APP_DIR)
|
||||||
TEMPLATES_DIR: str = os.path.join(APP_DIR, "templates")
|
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")
|
UPLOADS_DIR: str = os.path.join(STORAGE_DIR, "uploads")
|
||||||
PROCESSED_DIR: str = os.path.join(STORAGE_DIR, "processed")
|
PROCESSED_DIR: str = os.path.join(STORAGE_DIR, "processed")
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -58,7 +58,10 @@ app.add_middleware(
|
|||||||
# Mount storage directory (must come before general /static mount)
|
# Mount storage directory (must come before general /static mount)
|
||||||
app.mount("/static/audio", StaticFiles(directory=settings.STORAGE_DIR), name="audio")
|
app.mount("/static/audio", StaticFiles(directory=settings.STORAGE_DIR), name="audio")
|
||||||
# Mount app static files (js, css)
|
# 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")
|
||||||
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||||
|
|
||||||
# Include routers
|
# Include routers
|
||||||
@@ -75,6 +78,10 @@ app.include_router(plugins_router, prefix="/api/v1/plugins", tags=["plugins"])
|
|||||||
app.include_router(media_router, prefix="/api/v1/media", tags=["media"])
|
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)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def get_index():
|
async def get_index():
|
||||||
index_path = os.path.join(settings.TEMPLATES_DIR, "index.html")
|
index_path = os.path.join(settings.TEMPLATES_DIR, "index.html")
|
||||||
|
|||||||
+51
-8
@@ -14273,7 +14273,7 @@ const App = () => {
|
|||||||
setInstrumentDropdownTrackId(null);
|
setInstrumentDropdownTrackId(null);
|
||||||
setInstrumentDropdownBtnRect(null);
|
setInstrumentDropdownBtnRect(null);
|
||||||
setSynthCategory(null);
|
setSynthCategory(null);
|
||||||
// Trigger SpessaSynth load + program change when soundfont instrument selected
|
// Trigger FluidSynth load + program change when soundfont instrument selected
|
||||||
if (window.SonicSF && window.SonicSF.selectInstrument && instrumentId && isSfInstrument) {
|
if (window.SonicSF && window.SonicSF.selectInstrument && instrumentId && isSfInstrument) {
|
||||||
const sfId = instrumentId.replace('sf_', '');
|
const sfId = instrumentId.replace('sf_', '');
|
||||||
window.SonicSF.selectInstrument(mch, sfBank || 0, sfProg || 0, sfId);
|
window.SonicSF.selectInstrument(mch, sfBank || 0, sfProg || 0, sfId);
|
||||||
@@ -14500,6 +14500,11 @@ const App = () => {
|
|||||||
if (window.triggerMidiVuActivity) {
|
if (window.triggerMidiVuActivity) {
|
||||||
window.triggerMidiVuActivity(as.trackId, scaledVel);
|
window.triggerMidiVuActivity(as.trackId, scaledVel);
|
||||||
}
|
}
|
||||||
|
// Giữ VU theo trường độ: tăng counter note đang giữ
|
||||||
|
// (tick giữ peak cho tới 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);
|
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, asProg, null, asCh, asSe);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -14512,6 +14517,10 @@ const App = () => {
|
|||||||
if (window.triggerMidiVuActivity) {
|
if (window.triggerMidiVuActivity) {
|
||||||
window.triggerMidiVuActivity(at.id, scaledVel);
|
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);
|
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, atProg, atDest, atCh, atSe);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -14535,6 +14544,16 @@ const App = () => {
|
|||||||
if (!st.synth_engine && st.midiChannel === undefined) return;
|
if (!st.synth_engine && st.midiChannel === undefined) return;
|
||||||
var stCh = assignTrackMidiChannel(st, stopTracks);
|
var stCh = assignTrackMidiChannel(st, stopTracks);
|
||||||
window.SonicSF.stopNote(stCh, pitch);
|
window.SonicSF.stopNote(stCh, pitch);
|
||||||
|
// Giảm counter note đang giữ — hết note → VU được phép decay/tắt
|
||||||
|
// (tick sẽ thấy counter = 0 và không còn giữ peak nữa)
|
||||||
|
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 +15513,11 @@ const App = () => {
|
|||||||
}, [isPlaying]);
|
}, [isPlaying]);
|
||||||
|
|
||||||
const midiVuActivityRef = useRef({});
|
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: nhấn phím giữ âm còn kêu nhưng VU tắt
|
||||||
|
// sau ~0.5s vì decay 0.75/frame). Hết note (note-off) → mới decay/tắt.
|
||||||
|
const heldMidiNotesRef = useRef({});
|
||||||
const triggerMidiVuActivity = (trackId, velocity) => {
|
const triggerMidiVuActivity = (trackId, velocity) => {
|
||||||
if (!trackId) return;
|
if (!trackId) return;
|
||||||
const velFactor = typeof velocity === 'number' ? (velocity > 1 ? velocity / 127 : velocity) : 0.8;
|
const velFactor = typeof velocity === 'number' ? (velocity > 1 ? velocity / 127 : velocity) : 0.8;
|
||||||
@@ -20595,6 +20619,9 @@ const App = () => {
|
|||||||
// âm cũ — user 06:45: tắt âm MAIN items khi vào SECTION-TAB → VU main
|
// âm cũ — user 06:45: tắt âm MAIN items khi vào SECTION-TAB → VU main
|
||||||
// items phải tắt theo, không "diễn" tiếp).
|
// items phải tắt theo, không "diễn" tiếp).
|
||||||
try { midiVuActivityRef.current = {}; } catch (e) { }
|
try { midiVuActivityRef.current = {}; } catch (e) { }
|
||||||
|
// Clear luôn counter note đang giữ — nếu không, sau stop (âm đã dừng)
|
||||||
|
// tick vẫn thấy heldCnt > 0 → giữ peak → VU dính mãi (user bug 08:08).
|
||||||
|
try { heldMidiNotesRef.current = {}; } catch (e) { }
|
||||||
stopMidiCapture();
|
stopMidiCapture();
|
||||||
if (window.SonicSF) {
|
if (window.SonicSF) {
|
||||||
try { window.SonicSF.stopAll(); } catch (e) { console.warn('[Stop] stopAll error:', e); }
|
try { window.SonicSF.stopAll(); } catch (e) { console.warn('[Stop] stopAll error:', e); }
|
||||||
@@ -25744,8 +25771,18 @@ STRICT CONSTRAINTS:
|
|||||||
try {
|
try {
|
||||||
// Khi STOP (không play, không recording): vẽ VU RỖNG — không đọc analyser/
|
// Khi STOP (không play, không recording): vẽ VU RỖNG — không đọc analyser/
|
||||||
// activity → hết animation dính + master không full sau khi dừng play.
|
// activity → hết animation dính + master không full sau khi dừng play.
|
||||||
|
// ⚠️ NGOẠI LỆ: nếu có MIDI activity live (bật ARM track + bấm phím MIDI
|
||||||
|
// keyboard — KHÔNG play/record) thì KHÔNG early-return — track VU phải
|
||||||
|
// nhảy theo midiVuActivityRef + decay (user bug: ARM + phím MIDI → VU
|
||||||
|
// đứng yên vì block này vẽ rỗng và return trước khi đọc activity).
|
||||||
const isRecActive = activeAudioRecordersRef.current && Object.keys(activeAudioRecordersRef.current).length > 0;
|
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);
|
setMasterVU(0);
|
||||||
setMasterMeterPeak(0);
|
setMasterMeterPeak(0);
|
||||||
Object.keys(trackVuRefs.current).forEach(k => {
|
Object.keys(trackVuRefs.current).forEach(k => {
|
||||||
@@ -25846,12 +25883,18 @@ STRICT CONSTRAINTS:
|
|||||||
// velocity thấp (âm SF nhỏ < 0.03) → VU không nhảy (user bug 07:10).
|
// velocity thấp (âm SF nhỏ < 0.03) → VU không nhảy (user bug 07:10).
|
||||||
let midiPeak = isAudible ? (midiVuActivityRef.current[vuKey] || 0) : 0;
|
let midiPeak = isAudible ? (midiVuActivityRef.current[vuKey] || 0) : 0;
|
||||||
if (midiPeak > 0) {
|
if (midiPeak > 0) {
|
||||||
// Decay 0.75 (~0.5s) — cân bằng: note đơn/velocity thấp hiển thị rõ
|
// ARM + MIDI keyboard: còn note đang GIỮ → giữ nguyên peak, KHÔNG
|
||||||
// NHƯNG tắt nhanh sau hết note (decay 0.85 ~1s quá lâu — user:
|
// decay → VU animate đúng trường độ âm thanh (user bug 08:08: âm còn
|
||||||
// "vẫn diễn animation" sau khi âm hết — bug 07:15).
|
// kêu nhưng VU tắt sau ~0.5s vì decay 0.75/frame). Hết note → decay
|
||||||
midiVuActivityRef.current[vuKey] = midiPeak * 0.75;
|
// 0.75 (~0.5s) tắt nhanh như trước.
|
||||||
if (midiVuActivityRef.current[vuKey] < 0.01) {
|
const heldCnt = heldMidiNotesRef.current[vuKey] || 0;
|
||||||
midiVuActivityRef.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 gộp sfAudioPeak vào audioPeak: SF là 1 output CHUNG cho mọi
|
// ⚠️ KHÔNG gộp sfAudioPeak vào audioPeak: SF là 1 output CHUNG cho mọi
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -27,6 +27,16 @@ celery_app.conf.update(
|
|||||||
enable_utc=True,
|
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) ──
|
# ── Lịch trình tự động dọn dẹp file hết hạn (Week 5) ──
|
||||||
celery_app.conf.beat_schedule = {
|
celery_app.conf.beat_schedule = {
|
||||||
"cleanup-expired-files-every-hour": {
|
"cleanup-expired-files-every-hour": {
|
||||||
|
|||||||
@@ -3,8 +3,27 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<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>
|
<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">
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script src="https://unpkg.com/lucide@latest"></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/midiExtractor.js?v=202607281052"></script>
|
||||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></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/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=202608081400" defer></script>
|
||||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# build_windows.ps1 — ONE-COMMAND build: daw_engine.exe (PyInstaller) + Tauri v2 (NSIS + MSI)
|
||||||
|
# Chay tren Windows: powershell -ExecutionPolicy Bypass -File build_windows.ps1
|
||||||
|
$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
|
||||||
|
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 "== [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"
|
||||||
@@ -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()
|
||||||
@@ -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:
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
# 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 — datas PHẢI absolute: PyInstaller
|
||||||
|
# resolve duong dan relative theo CWD luc chay lenh (KHONG theo spec file),
|
||||||
|
# chay tu noi khac -> khong tim thay -> bo qua am tham -> app/static thieu
|
||||||
|
# trong bundle -> RuntimeError 'app\static does not exist' luc chay (da gap).
|
||||||
|
_SPEC_ROOT = os.path.abspath(SPECPATH)
|
||||||
|
|
||||||
|
# Assets doc (read-only) + thu muc storage (khoi tao rong; khi frozen,
|
||||||
|
# config.py chuyen storage sang %APPDATA%\SonicForgeDAW\storage)
|
||||||
|
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)
|
||||||
|
(os.path.join(_SPEC_ROOT, 'app', 'storage'), 'app/storage'),
|
||||||
|
(os.path.join(_SPEC_ROOT, 'md'), 'md'), # /ai-prompt-generator
|
||||||
|
]
|
||||||
|
|
||||||
|
# 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')
|
||||||
|
|
||||||
|
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',
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Default capability for the main window",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": ["core:default"]
|
||||||
|
}
|
||||||
@@ -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 |
@@ -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");
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
sonicforge_daw_lib::run()
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 8000–8010). 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>
|
||||||
@@ -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()
|
||||||
@@ -1,3 +1,37 @@
|
|||||||
|
### [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
|
### [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.
|
- **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)
|
- **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)
|
||||||
|
|||||||
Reference in New Issue
Block a user