From 0a1f3efd649e245ef0e70cd05b3b6ed6c3eab20f Mon Sep 17 00:00:00 2001 From: locpham Date: Sat, 8 Aug 2026 09:44:37 +0000 Subject: [PATCH] =?UTF-8?q?FEAT:=20chu=E1=BA=A9n=20b=E1=BB=8B=20th=C6=B0?= =?UTF-8?q?=20vi=E1=BB=87n=20=C4=91=E1=BB=83=20t=E1=BA=A1o=20file=20exe=20?= =?UTF-8?q?tr=C3=AAn=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 +- TEST_DEBUG_PLAN.md | 95 ++++++++++++++++++++++++++++ app/config.py | 22 ++++++- app/tasks/worker.py | 10 +++ build_windows.ps1 | 33 ++++++++++ desktop_engine.py | 87 +++++++++++++++++++++++++ engine.spec | 67 ++++++++++++++++++++ src-tauri/Cargo.toml | 23 +++++++ src-tauri/build.rs | 3 + src-tauri/capabilities/default.json | 7 ++ src-tauri/hooks.nsh | 7 ++ src-tauri/icons/128x128.png | Bin 0 -> 1009 bytes src-tauri/icons/128x128@2x.png | Bin 0 -> 1793 bytes src-tauri/icons/32x32.png | Bin 0 -> 459 bytes src-tauri/icons/icon.ico | Bin 0 -> 6297 bytes src-tauri/icons/icon.png | Bin 0 -> 3709 bytes src-tauri/src/lib.rs | 41 ++++++++++++ src-tauri/src/main.rs | 5 ++ src-tauri/tauri.conf.json | 41 ++++++++++++ src-tauri/ui/index.html | 35 ++++++++++ tools/gen_icons.py | 59 +++++++++++++++++ 21 files changed, 536 insertions(+), 4 deletions(-) create mode 100644 TEST_DEBUG_PLAN.md create mode 100644 build_windows.ps1 create mode 100644 desktop_engine.py create mode 100644 engine.spec create mode 100644 src-tauri/Cargo.toml create mode 100644 src-tauri/build.rs create mode 100644 src-tauri/capabilities/default.json create mode 100644 src-tauri/hooks.nsh create mode 100644 src-tauri/icons/128x128.png create mode 100644 src-tauri/icons/128x128@2x.png create mode 100644 src-tauri/icons/32x32.png create mode 100644 src-tauri/icons/icon.ico create mode 100644 src-tauri/icons/icon.png create mode 100644 src-tauri/src/lib.rs create mode 100644 src-tauri/src/main.rs create mode 100644 src-tauri/tauri.conf.json create mode 100644 src-tauri/ui/index.html create mode 100644 tools/gen_icons.py diff --git a/.gitignore b/.gitignore index 7f20662..82eae49 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,7 @@ samples/ .vscode/ *.log celerybeat-schedule -node_modules \ No newline at end of file +node_modules +src-tauri/target/ +src-tauri/binaries/ +src-tauri/vc_redist.x64.exe \ No newline at end of file diff --git a/TEST_DEBUG_PLAN.md b/TEST_DEBUG_PLAN.md new file mode 100644 index 0000000..5e1f099 --- /dev/null +++ b/TEST_DEBUG_PLAN.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` diff --git a/app/config.py b/app/config.py index 5fe73e1..15d9193 100644 --- a/app/config.py +++ b/app/config.py @@ -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") diff --git a/app/tasks/worker.py b/app/tasks/worker.py index b9e6d7e..67118fd 100644 --- a/app/tasks/worker.py +++ b/app/tasks/worker.py @@ -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": { diff --git a/build_windows.ps1 b/build_windows.ps1 new file mode 100644 index 0000000..dfff013 --- /dev/null +++ b/build_windows.ps1 @@ -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" diff --git a/desktop_engine.py b/desktop_engine.py new file mode 100644 index 0000000..d0f63bc --- /dev/null +++ b/desktop_engine.py @@ -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() diff --git a/engine.spec b/engine.spec new file mode 100644 index 0000000..1528ca6 --- /dev/null +++ b/engine.spec @@ -0,0 +1,67 @@ +# engine.spec — PyInstaller config cho daw_engine.exe (sidecar Python) +# Chay: pyinstaller engine.spec --clean --noconfirm (tren Windows) +# -*- mode: python ; coding: utf-8 -*- + +from PyInstaller.utils.hooks import collect_dynamic_libs + +# Native DLL cho pedalboard va soundfile +binaries = collect_dynamic_libs('pedalboard') +binaries += collect_dynamic_libs('soundfile') + +# Assets doc (read-only) + thu muc storage (khoi tao rong; khi frozen, +# config.py chuyen storage sang %APPDATA%\SonicForgeDAW\storage) +datas = [ + ('app/templates', 'app/templates'), # index.html, favicon.svg + ('app/static', 'app/static'), # js/css/processors + ('app/storage', 'app/storage'), + ('md', 'md'), # /ai-prompt-generator +] + +a = Analysis( + ['desktop_engine.py'], + pathex=['.'], + binaries=binaries, + datas=datas, + hiddenimports=[ + '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', +) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..9de4308 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -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 diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..230b422 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default capability for the main window", + "windows": ["main"], + "permissions": ["core:default"] +} diff --git a/src-tauri/hooks.nsh b/src-tauri/hooks.nsh new file mode 100644 index 0000000..ccec66d --- /dev/null +++ b/src-tauri/hooks.nsh @@ -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 diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..98477176cc512598d983679640dd767e4c8e25c3 GIT binary patch literal 1009 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrVBX>B;uumf=k49|-Xe)I$3LFW z(JR}q;Hq=S1ujRqPXZSg{#SS*+eE#e4l zlTzE(l)HIK`=zaS`3#NL8lUfx{`Ka}^Uu3Dc3Zz@zW?3EcsG0Lg@zdS^tBTor&RA; zx@hy&?Lfy$FkD?@tK$NrJByw{r_P4Bc)^3%zE(~J2aQ)F&t&o^rMv6EWX3b*by9UjGt-`*wJBh~amu&F#5}((ad*SVZUs;K z<#oTW-<ueyOz5Za^^YIJ<_o36z=$c@!0EU?_!UhUuxN{eb>^2G3EP~GhP49Nxv{DmB1_z*1K7}M+ew_dN(EqJjw*+IER63XxmNT5t zWGLclm_u*o^g?gfPe}^(&W;&0cCA-5;%Gb&K%?E8But1=~aC>ipS$ z>=t#|+o;l?S$6M?vp(a$BoW6?Khnj@zA^7(XewiLo!}FA`1|>)%NL4e=bB!YZri)^ zhIqQ)SM{nj42S9(P1G-QN=Y&#^f3f*oYl%o=i7Cr{oY^Zi;5mbGwhy9?&mO))i^0& wdiVzOwY&*Bh5q;2&J`TJFh}a<#{Ue{5Ax0Xe(ugUU{+@EboFyt=akR{09AgossI20 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3272f57584dd1eccff22e2f455e0b0dbcc56e760 GIT binary patch literal 1793 zcmeHI`BTz)82)_a5D)TbH{HxgE1PX5v7IE3#O)w+rlQeGGY`_%%!@5S6BSU))=guN zX|?dEHJ7{_6VF5nlUz-0jYUD!c3Xjv5D_p`Tz=i(_7CW%_nmoWo_Xh)=Xu`ZQzt{v zwwrAM0O-(TN5TMrEGq<1NXv-6`X~_q$TGGFwZ>jrILx`j{o&_J4-fSi8%GxPq`-Sy;?9+eQcmb^*l#tl6CII^ z_?nx_F9MK-L3iH@=IuW(m?p+A`&98fBIk9%ZowrD8f2hIuyDal@kh7eA=ltxj<+=R zOo_)pU)GG7M}*3ns(I`6#*8+9n|*YWSnGCxBKAd{*gmE>7&Aa#_NnmpNOI2KgBi@u#93rF0!ikQUxJw%Pn0H`o~;~}#ztKCf+iFQG$xUwLW--byaOQ0S}RK{ zNTtfAB&8kZ4nQAq(A5p$My>g;G3{lg?cyC)F6q5u4a|q`8Zm>$ZD7^}r#hsycSnlU zf?$o=2h*LZotks21W#84hCe7zD)F#|4-UFol~+z43YAK}MHgZ&Mosr#q7H53OLA<4 zjB;X1$K<;V&;cBegKg#DYafV7vI5WGpuh&eHv*K`C*VJlNcS2LK7kSHp4#RB%9$TZfLl3URNj+uJKLVr*L#I*5Zw)Cv2qC|CqTjK!oU;i z(9hPY{?`J%G=wa|#L*8JcJBQOLM^Ipwus-od=SM`k95D~aZ(nQQvKi0{^= zfxG^|B}KkiRbMZ;BH~it8>>TjUITM*$G?ULL-SUCPoRzqu%gLvhWPy?L&b)%Zs(mV zISvXc{Aisgw*)=?(8pMuH9IvIWzTN<g}g?!hIfk?~;3A6#LCxj55?|8))gZ{+BK#Q5Th`<)py9cc3in`0XjW8@xYfR^2?U z_>{Ia99hM%o6SN!&wbzeDmEc=GgGV24*#HE0ToD6bl!fy38v-B%p#i3ebqf0MWPkq usYE>riyWKTN9JClIbczbx##HZ225pS+oe6bBGyO$`mT6cI=?G}NS9Lnbx%g<~I-Ls2w@ zB19Q!n3RIqm2xOQ@-~EFHI%oyF87%Z?|C`TkHa~K^9U5lU^L07ASn?Q3S)h-aih6a z4SxW+0HP~YR5btKB^Zn*xlp(X2UG>P2?x-9)BEqz05kv%pzHwpQktvHR+{>32o)l7 zK1XnIjI10`HTnbwg7Dh(z8 z^wo8w?{5{E7VF15kWEPhmnLW#b&}6KBj;bZbd9SA0Fd;1Ncufr>c_*%e^X_D2{ix> zKm#Z{fBi`*HdHH?efAQHC(04eUjaH?j8!_N(WU?Z002ovPDHLkV1h=o B!-)U@ literal 0 HcmV?d00001 diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..84fc88da919ffe7b2d958e76eb6a8c61619e44ac GIT binary patch literal 6297 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 PR)YNj$Nh!-XqW#2tg>U9 literal 0 HcmV?d00001 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..91b5b8b --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,41 @@ +// SonicForge DAW — desktop shell: spawn/terminate daw_engine.exe sidecar. +use tauri_plugin_shell::process::CommandChild; +use tauri_plugin_shell::ShellExt; +use std::sync::Mutex; + +struct EngineProcess(Mutex>); + +#[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 { + let state = window.state::(); + if let Ok(mut lock) = state.0.lock() { + if let Some(child) = lock.take() { + let _ = child.kill(); + println!("daw_engine sidecar terminated."); + } + } + } + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..b407a77 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + sonicforge_daw_lib::run() +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..1d1c266 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,41 @@ +{ + "$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" + ], + "nsis": { + "installerHooks": "hooks.nsh" + } + } +} diff --git a/src-tauri/ui/index.html b/src-tauri/ui/index.html new file mode 100644 index 0000000..f7b1338 --- /dev/null +++ b/src-tauri/ui/index.html @@ -0,0 +1,35 @@ + + + + + SonicForge DAW + + + +
Đang khởi động SonicForge Engine…
+ + + diff --git a/tools/gen_icons.py b/tools/gen_icons.py new file mode 100644 index 0000000..666c9e3 --- /dev/null +++ b/tools/gen_icons.py @@ -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()