FEAT: cài đặt các thư viện để chuẩn bị build exe trên window
This commit is contained in:
@@ -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
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ os.makedirs(UPLOAD_SF_DIR, exist_ok=True)
|
|||||||
|
|
||||||
SYSTEM_SF_DIR = "/opt/daw_engine/soundfonts"
|
SYSTEM_SF_DIR = "/opt/daw_engine/soundfonts"
|
||||||
|
|
||||||
|
|
||||||
_inspector = None
|
_inspector = None
|
||||||
_scanner = None
|
_scanner = None
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ class PythonRenderEngine:
|
|||||||
# Parse synth_engine struct (Task C) — fall back to flat fields
|
# Parse synth_engine struct (Task C) — fall back to flat fields
|
||||||
se = track.get("synth_engine", {}) or {}
|
se = track.get("synth_engine", {}) or {}
|
||||||
instrument_id = se.get("plugin_id") or track.get("instrument_id", "") or track.get("instrument", "")
|
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")
|
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_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)
|
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:
|
if midi_events:
|
||||||
try:
|
try:
|
||||||
plugin_mgr = PluginManager()
|
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":
|
if instrument_source == "pianobook":
|
||||||
dspreset_path = track.get("dspreset_path", "")
|
dspreset_path = track.get("dspreset_path", "")
|
||||||
|
|||||||
+16
-2
@@ -216,6 +216,19 @@ class PluginManager:
|
|||||||
def load_vst(self, plugin_name: str, preset_data: dict = None):
|
def load_vst(self, plugin_name: str, preset_data: dict = None):
|
||||||
if not HAS_PEDALBOARD:
|
if not HAS_PEDALBOARD:
|
||||||
return None
|
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()
|
plugins = self._scan_plugins()
|
||||||
if plugin_name not in plugins:
|
if plugin_name not in plugins:
|
||||||
return None
|
return None
|
||||||
@@ -305,8 +318,9 @@ class PluginManager:
|
|||||||
def list_available(self) -> dict:
|
def list_available(self) -> dict:
|
||||||
return {
|
return {
|
||||||
"vst_instruments": [
|
"vst_instruments": [
|
||||||
{"id": k, "name": k, "type": "VST3", "has_native_support": HAS_PEDALBOARD}
|
{"id": "vst3_" + k.lower().replace(" ", "_"), "name": k, "type": "VST3",
|
||||||
for k in self._scan_plugins().keys()
|
"path": p, "has_native_support": HAS_PEDALBOARD}
|
||||||
|
for k, p in self._scan_plugins().items()
|
||||||
],
|
],
|
||||||
"soundfonts": self._scan_soundfonts()
|
"soundfonts": self._scan_soundfonts()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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_proxy import router as ai_proxy_router
|
||||||
from app.api.v1.ai_presets import router as ai_presets_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.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.api.v1.media import router as media_router
|
||||||
from app.core.auth import seed_admin
|
from app.core.auth import seed_admin
|
||||||
from app.core.soundfont_scanner import SoundFontAutoScanner
|
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_proxy_router, prefix="/api/v1/ai", tags=["ai"])
|
||||||
app.include_router(ai_presets_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(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"])
|
app.include_router(media_router, prefix="/api/v1/media", tags=["media"])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+86
-6
@@ -14193,6 +14193,12 @@ const App = () => {
|
|||||||
const [selectedSoundFontId, setSelectedSoundFontId] = useState(null);
|
const [selectedSoundFontId, setSelectedSoundFontId] = useState(null);
|
||||||
const [sfPresets, setSfPresets] = useState(null); // presets from SoundFont
|
const [sfPresets, setSfPresets] = useState(null); // presets from SoundFont
|
||||||
const [instrumentDropdownTrackId, setInstrumentDropdownTrackId] = useState(null);
|
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 [instrumentDropdownBtnRect, setInstrumentDropdownBtnRect] = useState(null);
|
||||||
const [instrumentSearchQuery, setInstrumentSearchQuery] = useState('');
|
const [instrumentSearchQuery, setInstrumentSearchQuery] = useState('');
|
||||||
const [sfPresetSearchQuery, setSfPresetSearchQuery] = 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",
|
"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"
|
"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 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 sfBank = bankNumber !== undefined ? bankNumber : (isSfInstrument ? 0 : undefined);
|
||||||
const sfProg = programNumber !== undefined ? programNumber : undefined;
|
const sfProg = programNumber !== undefined ? programNumber : undefined;
|
||||||
var mt = activeTracksRef.current || tracks;
|
var mt = activeTracksRef.current || tracks;
|
||||||
@@ -14264,6 +14271,12 @@ const App = () => {
|
|||||||
const synthEngine = hasInstrument ? {
|
const synthEngine = hasInstrument ? {
|
||||||
type: instrType,
|
type: instrType,
|
||||||
plugin_id: instrumentId,
|
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_bank: sfBank !== undefined ? sfBank : 0,
|
||||||
soundfont_program: sfProg !== undefined ? sfProg : 0,
|
soundfont_program: sfProg !== undefined ? sfProg : 0,
|
||||||
soundfont_id: isSfInstrument ? instrumentId.replace('sf_', '') : ''
|
soundfont_id: isSfInstrument ? instrumentId.replace('sf_', '') : ''
|
||||||
@@ -15114,7 +15127,7 @@ const App = () => {
|
|||||||
setAiPrompt(newText);
|
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);
|
const track = activeTracks.find(t => t.id === trackId);
|
||||||
if (!track) return;
|
if (!track) return;
|
||||||
const oldInstrumentId = track.instrumentId;
|
const oldInstrumentId = track.instrumentId;
|
||||||
@@ -15136,7 +15149,7 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (window.UndoRedoEngine) window.UndoRedoEngine.execute(entry);
|
if (window.UndoRedoEngine) window.UndoRedoEngine.execute(entry);
|
||||||
setTrackInstrumentWithProgram(trackId, instrumentId, programNumber, displayName, bankNumber);
|
setTrackInstrumentWithProgram(trackId, instrumentId, programNumber, displayName, bankNumber, vstMeta);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createSectionWithUndo = (trackId, section) => {
|
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.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", {
|
filteredInstruments.vst.map((v, i) => /*#__PURE__*/React.createElement("button", {
|
||||||
key: "vstd_" + i,
|
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"
|
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"))),
|
}, /*#__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'));
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
root.render(/*#__PURE__*/React.createElement(App, null));
|
root.render(/*#__PURE__*/React.createElement(App, null));
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -24,7 +24,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=202608080820" 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 {
|
||||||
|
|||||||
+73
@@ -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
|
||||||
|
)
|
||||||
@@ -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")
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
@@ -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 |
@@ -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");
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'))
|
||||||
@@ -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`.
|
- **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).
|
- **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.
|
- **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).
|
||||||
|
|||||||
Reference in New Issue
Block a user