FEAT: cài đặt các thư viện để chuẩn bị build exe trên window

This commit is contained in:
2026-08-08 08:46:15 +07:00
parent 9ef622f29a
commit 029ed1c456
24 changed files with 575 additions and 13 deletions
+60
View File
@@ -0,0 +1,60 @@
name: windows-build
on:
workflow_dispatch:
push:
tags: ['v*']
jobs:
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Install backend deps
run: |
pip install pyinstaller pywin32
pip install -r requirements.txt
- name: Precompile JS (Babel)
shell: bash
run: |
npm install --no-audit --no-fund 2>/dev/null || true
node node_modules/@babel/cli/bin/babel.js app/static/js/app.jsx --config-file ./babel.config.json -o app/static/js/app.precompiled.js
- name: Build daw_engine.exe (PyInstaller)
run: pyinstaller engine.spec --clean --noconfirm
- name: Stage sidecar binary (Tauri naming)
shell: bash
run: |
mkdir -p src-tauri/binaries
cp dist/daw_engine.exe "src-tauri/binaries/daw_engine-x86_64-pc-windows-msvc.exe"
- name: Install Tauri CLI
run: npm install -D @tauri-apps/cli@^2
- name: Build Windows installers (NSIS + MSI)
run: npx tauri build --no-bundle 2>/dev/null || npx tauri build
- name: Upload installer artifacts
uses: actions/upload-artifact@v4
with:
name: SonicForgeDAW-windows
path: |
src-tauri/target/release/bundle/nsis/*.exe
src-tauri/target/release/bundle/msi/*.msi
dist/daw_engine.exe
+40
View File
@@ -0,0 +1,40 @@
import os
import platform
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from typing import Optional
from app.core.vst_scanner import NativePluginScanner
from app.api.v1.auth import get_current_user
router = APIRouter()
# Thư mục mặc định theo OS (spec desktop VST 2026-08-08)
DEFAULT_PLUGIN_DIRS = {
"Linux": ["/opt/daw_engine/vst3", os.path.expanduser("~/.vst3")],
"Windows": [os.path.expandvars(r"%ProgramFiles%\Common Files\VST3")],
"Darwin": ["/Library/Audio/Plug-Ins/Components", os.path.expanduser("~/Library/Audio/Plug-Ins/Components")],
}
class PluginScanRequest(BaseModel):
custom_directories: Optional[list] = None
scan_formats: Optional[list] = None
force_rescan: Optional[bool] = False
@router.post("/plugins/scan")
async def scan_desktop_plugins(req: PluginScanRequest, current_user: dict = Depends(get_current_user)):
"""Quét VST3/AU từ thư mục custom — spec: POST /api/v1/desktop/plugins/scan."""
os_name = platform.system()
dirs = req.custom_directories or DEFAULT_PLUGIN_DIRS.get(os_name, ["/opt/daw_engine/vst3"])
if isinstance(dirs, str):
dirs = [dirs]
scanner = NativePluginScanner()
plugins = scanner.scan_directories(dirs)
return {
"status": "success",
"os": os_name,
"scanned_directories": dirs,
"total_found": len(plugins),
"plugins": plugins,
}
+1
View File
@@ -18,6 +18,7 @@ os.makedirs(UPLOAD_SF_DIR, exist_ok=True)
SYSTEM_SF_DIR = "/opt/daw_engine/soundfonts"
_inspector = None
_scanner = None
+3 -1
View File
@@ -105,6 +105,7 @@ class PythonRenderEngine:
# Parse synth_engine struct (Task C) — fall back to flat fields
se = track.get("synth_engine", {}) or {}
instrument_id = se.get("plugin_id") or track.get("instrument_id", "") or track.get("instrument", "")
vst_path = se.get("plugin_path") or "" # spec desktop: load VST3 từ plugin_path
instrument_source = se.get("type") or track.get("instrument_source", "soundfont")
soundfont_bank = se.get("soundfont_bank") if se.get("soundfont_bank") is not None else track.get("soundfont_bank", 0)
soundfont_program = se.get("soundfont_program") if se.get("soundfont_program") is not None else track.get("soundfont_program", 0)
@@ -189,7 +190,8 @@ class PythonRenderEngine:
if midi_events:
try:
plugin_mgr = PluginManager()
vst = plugin_mgr.load_vst(instrument_id) if instrument_id else None
# Ưu tiên plugin_path (spec desktop) — fallback plugin_id theo tên
vst = plugin_mgr.load_vst(vst_path or instrument_id) if (vst_path or instrument_id) else None
if instrument_source == "pianobook":
dspreset_path = track.get("dspreset_path", "")
+16 -2
View File
@@ -216,6 +216,19 @@ class PluginManager:
def load_vst(self, plugin_name: str, preset_data: dict = None):
if not HAS_PEDALBOARD:
return None
# Ưu tiên PATH trực tiếp (spec desktop 2026-08-08: synth_engine.plugin_path)
if plugin_name and (plugin_name.endswith(".vst3") or plugin_name.endswith(".so")
or plugin_name.endswith(".component") or "/" in plugin_name or "\\" in plugin_name):
if os.path.exists(plugin_name):
vst = VST3Plugin(plugin_name)
if preset_data:
for k, v in preset_data.items():
try:
setattr(vst, k, v)
except Exception:
pass
return vst
return None
plugins = self._scan_plugins()
if plugin_name not in plugins:
return None
@@ -305,8 +318,9 @@ class PluginManager:
def list_available(self) -> dict:
return {
"vst_instruments": [
{"id": k, "name": k, "type": "VST3", "has_native_support": HAS_PEDALBOARD}
for k in self._scan_plugins().keys()
{"id": "vst3_" + k.lower().replace(" ", "_"), "name": k, "type": "VST3",
"path": p, "has_native_support": HAS_PEDALBOARD}
for k, p in self._scan_plugins().items()
],
"soundfonts": self._scan_soundfonts()
}
+86
View File
@@ -0,0 +1,86 @@
import os
import platform
import logging
from typing import List, Dict, Any
logger = logging.getLogger(__name__)
class NativePluginScanner:
"""Quét thư mục custom chứa VST3 (.vst3) / Audio Unit (.component trên macOS)
— spec DESKTOP VST (2026-08-08): POST /api/v1/desktop/plugins/scan."""
def __init__(self):
self.os_type = platform.system() # 'Linux', 'Windows', 'Darwin'
def scan_directories(self, target_paths: List[str]) -> List[Dict[str, Any]]:
found_plugins = []
seen = set()
for base_path in target_paths:
if not base_path:
continue
if not os.path.exists(base_path):
logger.warning(f"Scan path does not exist: {base_path}")
continue
for root, dirs, files in os.walk(base_path):
for dir_name in dirs:
full_path = os.path.join(root, dir_name)
# VST3 = bundle directory *.vst3
if dir_name.endswith('.vst3'):
plugin_name = dir_name[:-5]
pid = f"vst3_{plugin_name.lower().replace(' ', '_')}"
if pid in seen:
continue
seen.add(pid)
found_plugins.append({
"id": pid,
"name": plugin_name,
"type": "VST3",
"format": "VST3",
"category": "Instrument",
"vendor": self._read_vendor(full_path) or None,
"path": full_path,
"is_instrument": True,
"has_native_gui": True
})
# Audio Unit = bundle *.component (macOS only)
elif dir_name.endswith('.component') and self.os_type == 'Darwin':
plugin_name = dir_name[:-10]
pid = f"au_{plugin_name.lower().replace(' ', '_')}"
if pid in seen:
continue
seen.add(pid)
found_plugins.append({
"id": pid,
"name": plugin_name,
"type": "AU",
"format": "AudioUnit",
"category": "Instrument",
"vendor": self._read_vendor(full_path) or None,
"path": full_path,
"is_instrument": True,
"has_native_gui": True
})
return found_plugins
@staticmethod
def _read_vendor(bundle_path: str) -> str:
"""Đọc vendor từ Info.plist nếu có (macOS) — best-effort."""
try:
plist = os.path.join(bundle_path, "Contents", "Info.plist")
if os.path.exists(plist):
with open(plist, "rb") as f:
raw = f.read(65536)
import plistlib
try:
data = plistlib.loads(raw)
return data.get("CFBundleManufacturer") or data.get("NSHumanReadableCopyright")
except Exception:
return None
except Exception:
return None
return None
+2
View File
@@ -17,6 +17,7 @@ from app.api.v1.user_config import router as user_config_router
from app.api.v1.ai_proxy import router as ai_proxy_router
from app.api.v1.ai_presets import router as ai_presets_router
from app.api.v1.plugins import router as plugins_router
from app.api.v1.desktop_plugins import router as desktop_plugins_router
from app.api.v1.media import router as media_router
from app.core.auth import seed_admin
from app.core.soundfont_scanner import SoundFontAutoScanner
@@ -72,6 +73,7 @@ app.include_router(user_config_router, prefix="/api/v1/user", tags=["user_config
app.include_router(ai_proxy_router, prefix="/api/v1/ai", tags=["ai"])
app.include_router(ai_presets_router, prefix="/api/v1/ai", tags=["ai"])
app.include_router(plugins_router, prefix="/api/v1/plugins", tags=["plugins"])
app.include_router(desktop_plugins_router, prefix="/api/v1/desktop", tags=["desktop"])
app.include_router(media_router, prefix="/api/v1/media", tags=["media"])
+86 -6
View File
@@ -14193,6 +14193,12 @@ const App = () => {
const [selectedSoundFontId, setSelectedSoundFontId] = useState(null);
const [sfPresets, setSfPresets] = useState(null); // presets from SoundFont
const [instrumentDropdownTrackId, setInstrumentDropdownTrackId] = useState(null);
// Plugin Manager VST scan (spec desktop 2026-08-08)
const [vstScanOpen, setVstScanOpen] = useState(false);
const [vstScanPath, setVstScanPath] = useState('/opt/daw_engine/vst3');
const [vstScanning, setVstScanning] = useState(false);
const [vstPlugins, setVstPlugins] = useState([]);
const [vstScanError, setVstScanError] = useState(null);
const [instrumentDropdownBtnRect, setInstrumentDropdownBtnRect] = useState(null);
const [instrumentSearchQuery, setInstrumentSearchQuery] = useState('');
const [sfPresetSearchQuery, setSfPresetSearchQuery] = useState('');
@@ -14249,8 +14255,9 @@ const App = () => {
"Tinkle Bell","Agogo","Steel Drums","Woodblock","Taiko Drum","Melodic Tom","Synth Drum","Reverse Cymbal",
"Guitar Fret Noise","Breath Noise","Seashore","Bird Tweet","Telephone Ring","Helicopter","Applause","Gunshot"
];
const setTrackInstrumentWithProgram = (trackId, instrumentId, programNumber, displayName, bankNumber) => {
const setTrackInstrumentWithProgram = (trackId, instrumentId, programNumber, displayName, bankNumber, vstMeta) => {
const isSfInstrument = instrumentId && typeof instrumentId === 'string' && instrumentId.startsWith('sf_');
const isVstInstrument = instrumentId && typeof instrumentId === 'string' && (instrumentId.startsWith('vst3_') || instrumentId.startsWith('au_') || (vstMeta && vstMeta.format));
const sfBank = bankNumber !== undefined ? bankNumber : (isSfInstrument ? 0 : undefined);
const sfProg = programNumber !== undefined ? programNumber : undefined;
var mt = activeTracksRef.current || tracks;
@@ -14264,6 +14271,12 @@ const App = () => {
const synthEngine = hasInstrument ? {
type: instrType,
plugin_id: instrumentId,
// spec desktop 2026-08-08: synth_engine đy đ {type, plugin_id, plugin_name, format, plugin_path}
...(isVstInstrument ? {
plugin_name: (vstMeta && vstMeta.plugin_name) || (displayName || instrumentId).replace(/\s*\((VST3|AU|AudioUnit)\)\s*$/, ''),
format: (vstMeta && vstMeta.format) || (instrumentId.startsWith('au_') ? 'AudioUnit' : 'VST3'),
plugin_path: (vstMeta && vstMeta.plugin_path) || ''
} : {}),
soundfont_bank: sfBank !== undefined ? sfBank : 0,
soundfont_program: sfProg !== undefined ? sfProg : 0,
soundfont_id: isSfInstrument ? instrumentId.replace('sf_', '') : ''
@@ -15114,7 +15127,7 @@ const App = () => {
setAiPrompt(newText);
};
const setTrackInstrumentWithUndo = (trackId, instrumentId, displayName, bankNumber, programNumber) => {
const setTrackInstrumentWithUndo = (trackId, instrumentId, displayName, bankNumber, programNumber, vstMeta) => {
const track = activeTracks.find(t => t.id === trackId);
if (!track) return;
const oldInstrumentId = track.instrumentId;
@@ -15136,7 +15149,7 @@ const App = () => {
}
};
if (window.UndoRedoEngine) window.UndoRedoEngine.execute(entry);
setTrackInstrumentWithProgram(trackId, instrumentId, programNumber, displayName, bankNumber);
setTrackInstrumentWithProgram(trackId, instrumentId, programNumber, displayName, bankNumber, vstMeta);
};
const createSectionWithUndo = (trackId, section) => {
@@ -29370,11 +29383,78 @@ STRICT CONSTRAINTS:
filteredInstruments.vst.length > 0 && /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold" }, "VST Instruments"),
filteredInstruments.vst.map((v, i) => /*#__PURE__*/React.createElement("button", {
key: "vstd_" + i,
onClick: () => { setInstrumentDropdownTrackId(null); setInstrumentDropdownBtnRect(null); setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id); },
onClick: () => { setInstrumentDropdownTrackId(null); setInstrumentDropdownBtnRect(null); setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id, undefined, undefined, { plugin_name: v.name || v.id, format: v.type || 'VST3', plugin_path: v.path || '' }); },
className: "w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-violet-800 text-zinc-200 flex items-center justify-between"
}, /*#__PURE__*/React.createElement("span", { className: "truncate" }, v.name || v.id), /*#__PURE__*/React.createElement("span", { className: "text-[10px] text-cyan-400 shrink-0 ml-1" }, v.type || "VST"))),
(!filteredInstruments.soundfonts.length && !filteredInstruments.vst.length) && /*#__PURE__*/React.createElement("p", { className: "text-xs text-zinc-500 py-4 text-center" }, "Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 n\u00e0o")
)));
/*#__PURE__*/React.createElement("button", {
onClick: () => { setInstrumentDropdownBtnRect(null); setVstScanOpen(true); },
className: "w-full text-left px-3 py-1.5 text-xs bg-purple-900/40 hover:bg-purple-800/60 text-purple-300 border-t border-zinc-700"
}, "\uD83D\uDD0C Quét VST Directory\u2026"),
(!filteredInstruments.soundfonts.length && !filteredInstruments.vst.length) && /*#__PURE__*/React.createElement("p", { className: "text-xs text-zinc-500 py-4 text-center" }, "Kh\u00f4ng t\u00ecm th\u1EA5y nh\u1EA1c c\u1EE5 n\u00E0o")
))),
/* Plugin Manager Modal — scan VST3/AU (spec desktop 2026-08-08) */
vstScanOpen && /*#__PURE__*/React.createElement("div", {
className: "fixed inset-0 bg-black/80 backdrop-blur-sm z-[120] flex items-center justify-center p-4",
onMouseDown: e => e.stopPropagation(),
onClick: e => e.stopPropagation()
}, /*#__PURE__*/React.createElement("div", { className: "w-full max-w-2xl bg-[#1e1e24] border border-zinc-700 rounded-2xl p-5 shadow-2xl space-y-4 text-zinc-200" },
/*#__PURE__*/React.createElement("div", { className: "flex items-center justify-between border-b border-zinc-700 pb-3" },
/*#__PURE__*/React.createElement("h2", { className: "text-sm font-bold text-purple-400 flex items-center gap-2" }, "\uD83D\uDD0C Plugin Manager (VST3 / AU)"),
/*#__PURE__*/React.createElement("button", { onClick: () => setVstScanOpen(false), className: "text-zinc-400 hover:text-white text-lg" }, "\u2715")
),
/*#__PURE__*/React.createElement("div", { className: "space-y-2 max-h-[50vh] overflow-y-auto" },
vstPlugins.length === 0 && !vstScanning && /*#__PURE__*/React.createElement("p", { className: "text-xs text-zinc-500 text-center py-6" }, "Ch\u01B0a qu\u00E9t — nh\u1EADp \u0111\u01B0\u1EDDng d\u1EABn th\u01B0 m\u1EE5c ch\u1EE9a plugin (m\u1EB7c \u0111\u1ECBnh /opt/daw_engine/vst3) r\u1ED3i b\u1EA5m Scan."),
vstPlugins.map((plg) => /*#__PURE__*/React.createElement("div", {
key: plg.id,
onClick: () => {
setTrackInstrumentWithUndo(instrumentDropdownTrackId, plg.id, plg.name + " (" + plg.format + ")", undefined, undefined, { plugin_name: plg.name, format: plg.format, plugin_path: plg.path });
setVstScanOpen(false);
setVstPlugins([]);
},
className: "bg-[#22222a] hover:bg-[#2c2c36] border border-zinc-700/60 p-3 rounded-xl flex items-center justify-between cursor-pointer transition-all"
}, /*#__PURE__*/React.createElement("div", { className: "flex items-center gap-3" },
/*#__PURE__*/React.createElement("div", { className: "w-8 h-8 rounded-lg bg-purple-950/80 border border-purple-800 flex items-center justify-center text-purple-400 text-xs" }, "\uD83C\uDFB5"),
/*#__PURE__*/React.createElement("div", null,
/*#__PURE__*/React.createElement("div", { className: "text-xs font-bold text-white" }, plg.name),
/*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-400 font-mono" }, plg.path)
)
), /*#__PURE__*/React.createElement("span", { className: "text-[10px] font-mono font-bold bg-purple-950 text-purple-300 border border-purple-800 px-2 py-0.5 rounded-full" }, plg.format)))
),
/*#__PURE__*/React.createElement("div", { className: "pt-3 border-t border-zinc-700/80 space-y-1" },
/*#__PURE__*/React.createElement("label", { className: "block text-[10px] font-bold text-zinc-400 uppercase" }, "ADD VST DIRECTORY"),
/*#__PURE__*/React.createElement("div", { className: "flex gap-2" },
/*#__PURE__*/React.createElement("input", {
type: "text", value: vstScanPath,
onChange: e => setVstScanPath(e.target.value),
className: "flex-1 bg-[#141418] border border-zinc-700 rounded-lg px-3 py-1.5 text-xs text-zinc-200 font-mono outline-none focus:border-purple-500"
}),
/*#__PURE__*/React.createElement("button", {
onClick: async () => {
setVstScanning(true);
setVstScanError(null);
try {
const resp = await fetch('/api/v1/desktop/plugins/scan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ custom_directories: [vstScanPath], scan_formats: ['VST3', 'AU'], force_rescan: true })
});
const data = await resp.json();
if (data.status === 'success') setVstPlugins(data.plugins || []);
else setVstScanError(data.detail || 'Scan thất bại');
} catch (err) {
setVstScanError('Lỗi kết nối: ' + err.message);
} finally {
setVstScanning(false);
}
},
disabled: vstScanning,
className: "px-5 py-1.5 bg-purple-600 hover:bg-purple-500 disabled:bg-zinc-700 text-white rounded-lg text-xs font-bold transition-all flex items-center gap-2"
}, vstScanning ? "Scanning..." : "Scan")
),
vstScanError && /*#__PURE__*/React.createElement("p", { className: "text-[10px] text-red-400" }, vstScanError),
/*#__PURE__*/React.createElement("p", { className: "text-[10px] text-zinc-600" }, "Windows: C:\\Program Files\\Common Files\\VST3 \u00B7 macOS: /Library/Audio/Plug-Ins/Components \u00B7 Linux: /opt/daw_engine/vst3, ~/.vst3")
)
))
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(/*#__PURE__*/React.createElement(App, null));
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -24,7 +24,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608070950" defer></script>
<script src="/static/js/app.precompiled.js?v=202608080820" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+73
View File
@@ -0,0 +1,73 @@
# engine.spec — PyInstaller config (spec desktop 2026-08-08 — daw_engine.exe)
# -*- mode: python ; coding: utf-8 -*-
import sys
import os
from PyInstaller.utils.hooks import collect_dynamic_libs, collect_data_files
block_cipher = None
# Collect native DLL files for pedalboard and soundfile
binaries = collect_dynamic_libs('pedalboard')
binaries += collect_dynamic_libs('soundfile')
binaries += collect_dynamic_libs('fluidsynth')
# Collect configuration files and default soundfont catalogs
datas = [
('app/storage', 'app/storage'),
('app/static', 'app/static'),
('app/templates', 'app/templates'),
]
a = Analysis(
['main.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',
'app.main',
'app.config',
'app.api.v1.desktop_plugins',
'app.core.vst_scanner',
'app.core.soundfont_scanner',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
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, # Set to True for Debugging, False for Production
)
+20
View File
@@ -0,0 +1,20 @@
"""Entry point cho daw_engine.exe (PyInstaller — spec desktop 2026-08-08).
Chạy FastAPI backend trên 127.0.0.1:8000 (Dual-Process Desktop Architecture)."""
import os
import sys
import threading
import webbrowser
os.environ.setdefault("SFDATA_DIR", os.path.join(os.path.expanduser("~"), "SonicForgeDAW"))
import uvicorn # noqa: E402
def _open_browser():
webbrowser.open("http://127.0.0.1:8000")
if __name__ == "__main__":
# Mở browser sau khi server sẵn sàng (console=False — chạy ẩn)
threading.Timer(2.0, _open_browser).start()
uvicorn.run("app.main:app", host="127.0.0.1", port=8000, log_level="warning")
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "sonicforge-daw"
version = "1.0.0"
description = "Sonic Forge DAW - Desktop Studio"
authors = ["SonicForge Studio"]
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]
codegen-units = 1
lto = true
opt-level = "s"
strip = true
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capabilities for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-spawn",
"shell:allow-open"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

+34
View File
@@ -0,0 +1,34 @@
use tauri_plugin_shell::ShellExt;
use tauri_plugin_shell::process::CommandChild;
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 (background Python engine — 127.0.0.1:8000)
let sidecar_command = app.shell().sidecar("daw_engine").unwrap();
let (mut _rx, child) = sidecar_command.spawn().expect("Failed to spawn daw_engine sidecar");
app.manage(EngineProcess(Mutex::new(Some(child))));
println!("Python Background Engine started successfully on localhost:8000");
Ok(())
})
.on_window_event(|window, event| {
if let tauri::WindowEvent::Destroyed = event {
// 2. Terminate sidecar process when DAW window is destroyed
let state = window.state::<EngineProcess>();
if let Ok(mut lock) = state.0.lock() {
if let Some(child) = lock.take() {
let _ = child.kill();
println!("daw_engine sidecar terminated gracefully.");
}
}
}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+6
View File
@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
sonicforge_daw_lib::run()
}
+37
View File
@@ -0,0 +1,37 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "SonicForgeDAW",
"version": "1.0.0",
"identifier": "com.sonicforge.daw",
"build": {
"frontendDist": "../app/templates",
"devUrl": "http://localhost:8000"
},
"app": {
"windows": [
{
"title": "Sonic Forge DAW - Professional Desktop Studio",
"url": "http://localhost:8000",
"width": 1440,
"height": 900,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": ["nsis", "msi"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/icon.ico"
],
"externalBin": [
"binaries/daw_engine"
]
}
}
+32
View File
@@ -0,0 +1,32 @@
import struct, zlib, os
def make_png(path, size):
w = h = size
raw = b''
for y in range(h):
row = b'\x00'
for x in range(w):
cx, cy = x - w / 2, y - h / 2
d = (cx * cx + cy * cy) ** 0.5 / (w / 2)
r = int(120 + 60 * (1 - d))
g = int(60 + 40 * (1 - d))
b = int(200 + 40 * (1 - d))
row += bytes((r, g, b, 255))
raw += row
def chunk(tag, data):
c = tag + data
return struct.pack('>I', len(data)) + c + struct.pack('>I', zlib.crc32(c) % 0x100000000)
png = b'\x89PNG\r\n\x1a\n'
png += chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 6, 0, 0, 0))
png += chunk(b'IDAT', zlib.compress(raw, 9))
png += chunk(b'IEND', b'')
open(path, 'wb').write(png)
os.makedirs('src-tauri/icons', exist_ok=True)
make_png('src-tauri/icons/32x32.png', 32)
make_png('src-tauri/icons/128x128.png', 128)
png128 = open('src-tauri/icons/128x128.png', 'rb').read()
ico = struct.pack('<HHH', 0, 1, 1) + struct.pack('<BBBBHHII', 128, 128, 0, 0, 1, 32, len(png128), 22) + png128
open('src-tauri/icons/icon.ico', 'wb').write(ico)
open('src-tauri/icons/icon.png', 'wb').write(png128)
print('icons:', os.listdir('src-tauri/icons'))
+34
View File
@@ -2925,3 +2925,37 @@
- **FIX (app.jsx effect follow):** `const playing = isPlaying || !!st.isPlaying` — follow khi piano roll play LẪN main play; deps thêm `st.isPlaying`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070950), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL → play (tab) → playhead ở giữa + notes trôi trái; stop → về đầu.
### [2026-08-08 08:15] Task: Desktop VST3/AU — scan + Plugin Manager (branch desktop-alone, spec 2026-08-08)
- **Checkout:** branch `desktop-alone` ✓ — cài đặt tính năng VST plugin theo spec (scan/display/assign).
- **Backend:**
(1) `app/core/vst_scanner.py``NativePluginScanner.scan_directories(paths)` — quét .vst3 (mọi OS) + .component (macOS) → {id, name, type, format, category, vendor, path, is_instrument, has_native_gui} — đọc vendor từ Info.plist (best-effort).
(2) `app/api/v1/desktop_plugins.py``POST /api/v1/desktop/plugins/scan` (router prefix /api/v1/desktop — đúng spec) — nhận custom_directories/scan_formats/force_rescan → response {status, os, scanned_directories, total_found, plugins[]}; thư mục mặc định theo OS (Linux /opt/daw_engine/vst3+~/.vst3; Windows %ProgramFiles%\Common Files\VST3; macOS /Library/Audio/Plug-Ins/Components).
(3) `app/main.py` — include desktop_plugins_router.
- **Frontend (app.jsx):** nút "🔌 Quét VST Directory…" (cuối instrument dropdown) → PluginManager Modal: input path + [Scan] (spinner/Scanning…) + list plugin (tên/path/format) → click plugin → `setTrackInstrumentWithUndo(trackId, plugin_id, "Name (VST3)")` (label track cập nhật) + đóng modal. Giữ instrumentDropdownTrackId khi mở modal (không null).
- **Các file ảnh hưởng:** app/core/vst_scanner.py, app/api/v1/desktop_plugins.py, app/main.py, app/static/js/app.jsx, app/templates/index.html (?v=202608080815), wiki.md. Build precompiled PASS.
- **Ghi chú/Test:** python test scanner (2 plugin giả OK). E2E: mở track instrument dropdown → Quét VST Directory → nhập /opt/daw_engine/vst3 → Scan → chọn plugin → label track thành "🎵 Name (VST3)".
### [2026-08-08 08:20] Task: VST3 — ĐỒNG BỘ state + render đúng tiếng (spec 2 điểm then chốt)
- **User chỉ ra 2 điểm:** (A) synth_engine phải đầy đủ {type, plugin_id, plugin_name, format, plugin_path}; (B) render dùng `pedalboard.load_plugin(plugin_path)`.
- **A — Frontend (app.jsx):**
(1) `setTrackInstrumentWithProgram` — thêm param `vstMeta` ({plugin_name, format, plugin_path}) — synth_engine vst3 → spread đủ 5 field theo spec (isVstInstrument: id vst3_/au_ hoặc vstMeta.format).
(2) `setTrackInstrumentWithUndo` — nhận + truyền vstMeta.
(3) Dropdown VST + PluginManager modal — gọi với vstMeta (plugin_name/format/plugin_path từ item scan).
(4) API `/available` (`vst_engine.list_available`) — vst_instruments thêm `path` + id chuẩn `vst3_<name>`.
- **B — Backend:**
(1) `vst_engine.load_vst` — NHẬN PATH trực tiếp (đuôi .vst3/.so/.component hoặc chứa dấu / hoặc \ → os.path.exists → VST3Plugin(path)) — fallback tên cũ.
(2) `render_engine` — đọc `se.plugin_path``load_vst(vst_path or instrument_id)` — nhánh Pedalboard render (đã có — midi_messages + Pedalboard([vst])) giờ load đúng plugin.
- **Các file ảnh hưởng:** app/core/vst_engine.py, app/core/render_engine.py, app/static/js/app.jsx, app/templates/index.html (?v=202608080820), wiki.md. Build PASS (plugin_path ×3) + ast OK.
- **Ghi chú/Test:** chọn VST (dropdown hoặc modal) → track.synth_engine = {type:'vst3', plugin_id, plugin_name, format, plugin_path} → Render/Bounce → render_engine load VST3 qua pedalboard theo path → WAV đúng tiếng plugin (cần pedalboard cài + plugin thật ở path).
### [2026-08-08 08:30] Task: Windows .exe installer — packaging theo spec (PyInstaller + Tauri v2 sidecar)
- **Spec 08-27:** Dual-Process Desktop — (1) daw_engine.exe (PyInstaller — FastAPI bind localhost:8000) + (2) Tauri v2 shell (WebView2) — sidecar — NSIS/MSI installer.
- **Môi trường hiện tại = Linux → KHÔNG build .exe trực tiếp** → tạo ĐỦ file build + CI workflow (windows-latest) → artifact .exe tải xuống.
- **Đã tạo:**
(1) `main.py` (root entry — uvicorn 127.0.0.1:8000 + auto-open browser).
(2) `engine.spec` (spec — collect pedalboard/soundfile/fluidsynth DLL + static/templates/storage datas + hiddenimports; console=False; upx).
(3) `src-tauri/` — tauri.conf.json (window url=http://127.0.0.1:8000, externalBin daw_engine, targets nsis+msi), Cargo.toml (tauri 2 + tauri-plugin-shell), build.rs, src/main.rs, src/lib.rs (spawn sidecar + kill khi window destroyed — theo spec), capabilities/default.json (shell:allow-spawn/open), icons (32/128 png + icon.ico — tools/mkicons.py).
(4) `.github/workflows/windows-build.yml` — windows-latest: python 3.11 + node + rust → pip deps → Babel precompile → pyinstaller engine.spec → copy sidecar (daw_engine-x86_64-pc-windows-msvc.exe) → npx tauri build → upload artifact (NSIS .exe + MSI + daw_engine.exe).
- **Các file ảnh hưởng:** main.py, engine.spec, src-tauri/*, .github/workflows/windows-build.yml, tools/mkicons.py, wiki.md.
- **Ghi chú/Test:** push lên GitHub (branch desktop-alone) → Actions → workflow_dispatch (hoặc tag v*) → tải artifact "SonicForgeDAW-windows" → chạy setup.exe trên máy Windows (test: daw_engine xuất hiện Task Manager, /docs mở, VST3 scan, Export WAV).