fix(fxrt): mirror sidecar exe clean build, probe toggle do latency thuc (Ozone) - cache HIT xac nhan khong reload khi toggle

This commit is contained in:
2026-08-24 17:05:13 +07:00
parent 59e4951912
commit 6ec249ef13
3 changed files with 182 additions and 0 deletions
+182
View File
@@ -0,0 +1,182 @@
# fxrt_toggle_probe.py — đo toggle bypass dry->wet với VST instance cache.
# Kịch bản THẬT: start với slot ACTIVE (load + cache) -> toggle bypass=true
# (chain đổi, KHÔNG unload) -> toggle bypass=false (cache hit, không reload)
# Đo: thời gian set_chain -> latency report (chain applied) mỗi toggle.
# python fxrt_toggle_probe.py [--port 8012]
import asyncio
import ctypes
import json
import os
import subprocess
import sys
import tempfile
import time
import numpy as np
REPO = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, REPO)
SR = 48000
BLOCK = 256
BATCH = 4
BATCH_PERIOD = BATCH * BLOCK / SR # 21.333ms
ADELAY = os.path.join(REPO, "native_bridge", "build", "VST3", "Release", "adelay.vst3")
OZONE_MAX = r"C:\Program Files\Common Files\VST3\iZotope\Ozone 11 Maximizer.vst3"
PLUGIN = os.environ.get("FXRT_PROBE_PLUGIN", OZONE_MAX) # mặc định Ozone (latency thật 747smp)
def make_block():
t = np.arange(BLOCK) / SR
amp = float(os.environ.get("FXRT_PROBE_AMP", "1.0"))
sig = amp * (0.2 * np.sin(2 * np.pi * 220 * t) + 0.1 * np.sin(2 * np.pi * 331 * t))
inter = np.empty(BLOCK * 2, dtype=np.float32)
inter[0::2] = sig
inter[1::2] = 0.85 * sig
return inter.tobytes()
def chain_with(bypass):
return [{"type": "vst3", "path": PLUGIN, "name": os.path.basename(PLUGIN),
"preset_b64": "", "bypass": bypass}]
async def probe(port):
import websockets
import urllib.request
base = "http://127.0.0.1:%d" % port
block_bytes = make_block()
def post(path, obj):
req = urllib.request.Request(
base + path, data=json.dumps(obj).encode(),
headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
return json.loads(r.read().decode())
st = post("/api/v1/plugins/fx-realtime/start",
{"fx_chain": chain_with(False), "sample_rate": SR})
sid = st["session_id"]
print("session=%s started slot ACTIVE (load + cache)" % sid)
ws_url = "ws://127.0.0.1:%d/api/v1/plugins/ws/fx-realtime/%s" % (port, sid)
lat_events = [] # (time, seq)
stop = asyncio.Event()
started = asyncio.Event()
async def producer():
next_t = time.perf_counter() + 0.3
while not stop.is_set():
await asyncio.to_thread(time.sleep, max(0, next_t - time.perf_counter()))
if started.is_set():
await ws.send(block_bytes)
next_t += BATCH_PERIOD
async def consumer():
while not stop.is_set():
try:
msg = await ws.recv()
except Exception:
break
if not isinstance(msg, bytes):
try:
d = json.loads(msg)
except Exception:
continue
if isinstance(d, dict) and d.get("cmd") == "latency":
lat_events.append((time.perf_counter(), d.get("total")))
async with websockets.connect(ws_url) as ws:
tasks = [asyncio.create_task(producer()), asyncio.create_task(consumer())]
await asyncio.sleep(0.5)
started.set()
await asyncio.sleep(1.5) # để start-load + latency report đầu
n0 = len(lat_events)
print("baseline latency reports:", lat_events[n0 - 3:n0])
# toggle 1: bypass=true (chain đổi, instance GIỮ trong cache)
t0 = time.perf_counter()
post("/api/v1/plugins/fx-realtime/set-chain",
{"session_id": sid, "fx_chain": chain_with(True)})
await asyncio.sleep(0.6)
t1 = time.perf_counter()
new = [x for x in lat_events if x[0] >= t0]
print("TOGGLE1 bypass=true -> chain applied in %s (%.0fms)" %
("%.0fms" % ((new[0][0] - t0) * 1e3) if new else "NOT SEEN", 0))
# toggle 2: bypass=false (CACHE HIT — không reload)
t2 = time.perf_counter()
post("/api/v1/plugins/fx-realtime/set-chain",
{"session_id": sid, "fx_chain": chain_with(False)})
await asyncio.sleep(0.6)
t3 = time.perf_counter()
new2 = [x for x in lat_events if x[0] >= t2]
print("TOGGLE2 bypass=false -> chain applied in %s" %
("%.0fms" % ((new2[0][0] - t2) * 1e3) if new2 else "NOT SEEN"))
# toggle 3: bypass=true lần 2 (cache hit)
t4 = time.perf_counter()
post("/api/v1/plugins/fx-realtime/set-chain",
{"session_id": sid, "fx_chain": chain_with(True)})
await asyncio.sleep(0.6)
new3 = [x for x in lat_events if x[0] >= t4]
print("TOGGLE3 bypass=true -> chain applied in %s" %
("%.0fms" % ((new3[0][0] - t4) * 1e3) if new3 else "NOT SEEN"))
stop.set()
await asyncio.sleep(0.3)
for t in tasks:
t.cancel()
try:
post("/api/v1/plugins/fx-realtime/stop", {"session_id": sid})
except Exception:
pass
print("all latency events:", ["%.2fs/%.0f" % (x[0] - lat_events[0][0], x[1] or 0) for x in lat_events])
def main():
try:
ctypes.windll.winmm.timeBeginPeriod(1)
except Exception:
pass
port = 8012
if "--port" in sys.argv:
port = int(sys.argv[sys.argv.index("--port") + 1])
storage = tempfile.mkdtemp(prefix="sf_toggleprobe_")
env = dict(os.environ)
env["SONICFORGE_STORAGE_DIR"] = storage
env["FXRT_BRIDGE_LOG"] = os.path.join(REPO, "fxrt_toggle_bridge_%d.log" % port)
logf = open(os.path.join(REPO, "fxrt_toggle_engine_%d.log" % port), "ab", buffering=0)
boot = ("import ctypes; ctypes.windll.winmm.timeBeginPeriod(1); "
"import uvicorn, app.main; "
"uvicorn.run(app.main.app, host='127.0.0.1', port=%d, log_level='warning')" % port)
engine = subprocess.Popen(
[sys.executable, "-c", boot],
cwd=REPO, env=env, stdout=logf, stderr=subprocess.STDOUT,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
try:
import urllib.request
deadline = time.time() + 60
while time.time() < deadline:
try:
with urllib.request.urlopen("http://127.0.0.1:%d/health" % port, timeout=2) as r:
if r.status == 200:
break
except Exception:
time.sleep(0.5)
else:
print("engine not up; log tail:")
print(open(logf.name, "rb").read().decode(errors="replace")[-2000:])
return
print("engine up on :%d" % port)
asyncio.run(probe(port))
finally:
engine.terminate()
try:
engine.wait(timeout=5)
except Exception:
engine.kill()
logf.close()
if __name__ == "__main__":
main()
Binary file not shown.