FIX: 6 lỗi âm thanh/Carla/temp-save/Ctrl-S/FX Chain Carla bridge
- Soundfont preview: hủy note đang chờ load soundfont (stopNote/stopAll/panic) — hết âm loop không dừng với MIDI Keyboard; preview dùng channel riêng (applyAITrackInstrument) — hết sai instrument - Carla bridge: endpoint /carla-stop (all-notes-off OSC + terminate process) + stopBridge() khi track chuyển VSTi -> soundfont — hết âm play qua Carla cũ - MIDI items play qua Carla khi VSTi loaded: scheduleCarlaNote route vào startTrackPlayback + startLocalTrackPlayback + ghost notes - Tự động lưu temp khi tắt app: beforeunload/pagehide sendBeacon + autosave 30s + ghi storage/temp/autosave.json + khôi phục khi load lại - Ctrl-S: desktop -> save-to-disk (Documents/SonicForgeDAW/Projects); docker -> Cloud/local - FX Chain (Mastering + FX Rack): module Carla Bridge (VST FX) — load/openInCarla + stop/unload, pass-through trong graph
This commit is contained in:
+81
-6
@@ -162,6 +162,18 @@ async def save_temp_project(req: SaveTempProjectRequest, current_user: Optional[
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
# ── Ghi file autosave vào thư mục temp CỦA ỨNG DỤNG (storage/temp) ──
|
||||
# Yêu cầu: "Khi tắt ứng dụng → tự động lưu temp trên thư mục temp của ứng
|
||||
# dụng để khi load lại thì tải lại dự án đang làm dở." Ngoài row trong DB,
|
||||
# ghi thẳng file JSON để luôn có bản sao thật trên ổ đĩa OS.
|
||||
try:
|
||||
from app.config import settings as _st
|
||||
temp_dir = os.path.join(_st.STORAGE_DIR, "temp")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
with open(os.path.join(temp_dir, "autosave.json"), "w", encoding="utf-8") as f:
|
||||
json.dump({"user_id": user_id, "updated_at": now, "data_json": validated_data_json}, f, ensure_ascii=False)
|
||||
except Exception:
|
||||
pass
|
||||
return {"message": "Đã lưu dự án tạm tự động", "updated_at": now}
|
||||
|
||||
@router.get("/temp")
|
||||
@@ -175,15 +187,78 @@ async def get_temp_project(current_user: Optional[dict] = Depends(get_optional_u
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not row:
|
||||
return {"has_temp": False}
|
||||
|
||||
if row:
|
||||
return {
|
||||
"has_temp": True,
|
||||
"data_json": row["data_json"],
|
||||
"updated_at": row["updated_at"]
|
||||
}
|
||||
# Fallback: file autosave.json trong thư mục temp của ứng dụng (khi lưu lúc
|
||||
# đóng app qua sendBeacon — user anonymous) — tải lại dự án đang làm dở.
|
||||
try:
|
||||
from app.config import settings as _st
|
||||
autosave_path = os.path.join(_st.STORAGE_DIR, "temp", "autosave.json")
|
||||
if os.path.isfile(autosave_path):
|
||||
with open(autosave_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if data.get("data_json"):
|
||||
return {
|
||||
"has_temp": True,
|
||||
"data_json": data["data_json"],
|
||||
"updated_at": data.get("updated_at", 0),
|
||||
"source": "file",
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
return {"has_temp": False}
|
||||
|
||||
def _os_projects_dir() -> str:
|
||||
"""Thư mục dự án trên hệ điều hành (Ctrl-S desktop):
|
||||
Windows → Documents/SonicForgeDAW/Projects (fallback USERPROFILE);
|
||||
Linux/macOS → ~/SonicForgeDAW/Projects. Luôn tồn tại (tự tạo)."""
|
||||
try:
|
||||
if os.name == "nt":
|
||||
docs = os.path.join(os.environ.get("USERPROFILE") or os.path.expanduser("~"), "Documents")
|
||||
base = docs if os.path.isdir(docs) else (os.environ.get("USERPROFILE") or os.path.expanduser("~"))
|
||||
else:
|
||||
base = os.path.expanduser("~")
|
||||
d = os.path.join(base, "SonicForgeDAW", "Projects")
|
||||
os.makedirs(d, exist_ok=True)
|
||||
return d
|
||||
except Exception:
|
||||
return os.path.join(os.path.expanduser("~"), "SonicForgeDAW", "Projects")
|
||||
|
||||
|
||||
@router.post("/save-to-disk")
|
||||
async def save_project_to_disk(req: SaveProjectRequest, authorization: Optional[str] = Header(None)):
|
||||
"""Ctrl-S trên desktop: lưu project ra THƯ MỤC CỦA HỆ ĐIỀU HÀNH
|
||||
(Documents/SonicForgeDAW/Projects — bản desktop). Docker/headless KHÔNG
|
||||
dùng endpoint này (frontend lưu Cloud). Auth optional — desktop có thể
|
||||
chưa login."""
|
||||
try:
|
||||
if authorization and authorization.startswith("Bearer "):
|
||||
decode_token(authorization.split(" ")[1])
|
||||
except Exception:
|
||||
pass
|
||||
validated_data_json = validate_project_data(req.data_json)
|
||||
safe_name = "".join(c for c in (req.name or "Dự án mới") if c.isalnum() or c in " _-.").strip() or "Du-an-moi"
|
||||
if len(safe_name) > 80:
|
||||
safe_name = safe_name[:80].strip()
|
||||
safe_name = safe_name.replace(".", "_") if safe_name.endswith(".") else safe_name
|
||||
fname = safe_name + ".sonicforge.json"
|
||||
out_dir = _os_projects_dir()
|
||||
out_path = os.path.join(out_dir, fname)
|
||||
# Không ghi đè file đang mở ở nơi khác? Ghi đè OK (Ctrl-S = save).
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
f.write(validated_data_json)
|
||||
return {
|
||||
"has_temp": True,
|
||||
"data_json": row["data_json"],
|
||||
"updated_at": row["updated_at"]
|
||||
"success": True,
|
||||
"name": req.name or "Dự án mới",
|
||||
"path": out_path,
|
||||
"filename": fname,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/cloud")
|
||||
async def save_cloud_project(req: SaveProjectRequest, current_user: dict = Depends(get_current_user)):
|
||||
validated_data_json = validate_project_data(req.data_json)
|
||||
|
||||
Reference in New Issue
Block a user