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:
+112
-3
@@ -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í
|
||||
|
||||
Reference in New Issue
Block a user