fix(fx): realtime VST ngắt quãng khi mở MASTERING PANEL + PWR master không ảnh hưởng âm; pacing WS input/output, gate fxRt theo masterConnected, worklet cap 32

This commit is contained in:
2026-08-21 21:48:56 +07:00
parent 4e3f2fd08a
commit bb6658e03d
17 changed files with 1813 additions and 142 deletions
+112 -3
View File
@@ -1,7 +1,7 @@
import os, sys, uuid, json, tempfile, subprocess, time as _time, threading
import os, sys, uuid, json, tempfile, subprocess, time as _time, threading, asyncio
import numpy as np
import soundfile as sf
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Header, Query
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Header, Query, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse
from pydantic import BaseModel
from typing import Optional, Any
@@ -12,6 +12,8 @@ from app.core.soundfont_inspector import SoundFontInspector
from app.core.soundfont_converter import SoundFontConverter
from app.core.soundfont_scanner import SoundFontAutoScanner
from app.api.v1.auth import get_current_user, enforce_password_changed
from app.core import fx_realtime
from app.core.auth import decode_token
router = APIRouter()
@@ -1179,7 +1181,12 @@ async def fx_render(req: FxRenderRequest, current_user: dict = Depends(get_curre
if not fid:
raise HTTPException(status_code=400,
detail="Cần input_file_id hoặc input_path")
src = os.path.join(settings.PROCESSED_DIR, os.path.basename(fid))
# File từ client upload (audio/upload → UPLOADS_DIR) hoặc đã xử lý
# (PROCESSED_DIR) — resolve cả hai như audio.download.
fid = os.path.basename(fid)
src = os.path.join(settings.PROCESSED_DIR, fid)
if not os.path.isfile(src):
src = os.path.join(settings.UPLOADS_DIR, fid)
if not os.path.isfile(src):
raise HTTPException(status_code=404,
detail=f"File đầu vào không tồn tại: {src}")
@@ -1222,6 +1229,108 @@ def _render_fx_locked(input_path: str, fx_chain: list, sample_rate: int, out_pat
sample_rate=sample_rate, out_path=out_path)
class FxRealtimeStartRequest(BaseModel):
"""Start một session FX realtime (master chain qua fx_vst_bridge
--realtime-fx + SHM ring). Trả session_id để client mở WebSocket
/ws/fx-realtime/{session_id} đẩy/kéo block audio."""
fx_chain: list = []
sample_rate: int = 44100
class FxRealtimeStopRequest(BaseModel):
session_id: str = ""
@router.post("/fx-realtime/start")
async def fx_realtime_start(req: FxRealtimeStartRequest,
current_user: dict = Depends(get_current_user)):
"""Start bridge realtime FX loop + SHM. Client mở WS /ws/fx-realtime/{id}
(token qua query param 'token' hoặc Authorization Bearer) rồi gửi block
stereo float32 interleaved (block_size*2 float = 2048B), nhận block đã xử lý."""
enforce_password_changed(current_user)
try:
sess = fx_realtime.start_session(req.fx_chain, req.sample_rate)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,
detail=f"Không khởi động FX realtime: {e}")
return {
"success": True,
"session_id": sess.name,
"ws_url": f"/api/v1/plugins/ws/fx-realtime/{sess.name}",
"block_size": sess.block_size,
"sample_rate": sess.sample_rate,
}
@router.post("/fx-realtime/stop")
async def fx_realtime_stop(req: FxRealtimeStopRequest,
current_user: dict = Depends(get_current_user)):
"""Dừng session realtime (running=0 → bridge thoát, SHM unlink)."""
enforce_password_changed(current_user)
ok = fx_realtime.stop_session(req.session_id or "")
return {"success": True, "stopped": ok}
@router.websocket("/ws/fx-realtime/{session_id}")
async def ws_fx_realtime(websocket: WebSocket, session_id: str):
"""Vòng lặp FX realtime: receive block input → SHM → bridge → SHM → send
block output. Binary frames: 2048B stereo float32 interleaved (256*2)."""
# Auth: token qua query param (JS WebSocket không set header dễ) hoặc
# Authorization Bearer. Desktop app có thể chưa login → token rỗng được
# phép (endpoint /start vẫn yêu cầu auth).
token = websocket.query_params.get("token", "") or ""
if not token:
auth = websocket.headers.get("authorization", "")
if auth.lower().startswith("bearer "):
token = auth.split(" ", 1)[1].strip()
if token:
try:
payload = decode_token(token)
except Exception:
payload = None
if not payload:
await websocket.close(code=4401)
return
sess = fx_realtime.get_session(session_id)
if not sess:
await websocket.close(code=4404)
return
await websocket.accept()
# Output pump riêng: bridge sản xuất block KHÔNG đồng bộ với input client —
# nếu chỉ drain trong vòng receive_bytes, output thừa kẹt lại trong SHM ring
# (client ngừng gửi → server block ở receive → âm tắc).
async def _pump_output():
# Pace mỗi block theo tốc độ âm thật (256/sr ~ 5.8ms): bridge sản xuất
# output theo burst khi nhận input dồn; bắn toàn bộ cùng lúc → worklet
# overflow drop block cũ nhất → âm ngắt quãng.
block_sec = (sess.block_size or 256) / max(int(sess.sample_rate or 44100), 1)
try:
while True:
blks = sess.read_output()
if not blks:
await asyncio.sleep(0.002)
continue
for blk in blks:
await websocket.send_bytes(blk)
await asyncio.sleep(block_sec)
except Exception:
pass
pump_task = asyncio.create_task(_pump_output())
try:
while True:
data = await websocket.receive_bytes()
if not sess.alive():
break
sess.write_input(data)
except WebSocketDisconnect:
pass
except Exception:
pass
finally:
try:
pump_task.cancel()
except Exception:
pass
fx_realtime.stop_session(session_id)
class SoundfontRenderRequest(BaseModel):
"""Render MIDI notes → WAV bằng soundfont THẬT qua native FluidSynth
(pyfluidsynth) — luồng âm instrument cho môi trường STANDALONE (xử lí