P0: cat torch/torchvision khoi engine bundle (1842MB -> 147MB) + hop dong API + golden conformance + baseline

This commit is contained in:
2026-10-12 21:03:17 +07:00
parent 7c236d472f
commit 322748898b
8 changed files with 7749 additions and 6 deletions
+7
View File
@@ -137,6 +137,13 @@ _excludes = [
'matplotlib', 'pandas', 'IPython', 'jupyter', 'pytest', 'setuptools',
# keo vao nham boi hooks_contrib (app khong import bao gio)
'PIL', 'Pillow', 'cairosvg', 'zstandard', 'imageio',
# P0 (2026-09): torch/torchvision (~1.5GB) bi keo vao boi
# scipy._lib.array_api_compat.common._helpers ('import torch' trong try/except).
# App KHONG import torch o bat ky dau - da kiem chung: block import torch ->
# scipy.signal + moi module app.core import van OK. Loai ca cay AI nang.
'torch', 'torchvision', 'torchaudio', 'triton',
'sympy', 'networkx', 'tqdm', 'einops', 'optree',
'tensorflow', 'keras', 'jax', 'jaxlib',
]
a = Analysis(
+46
View File
@@ -0,0 +1,46 @@
# Baseline P0 - SonicForgeStudio (Python engine)
Sinh boi `tools/p0_baseline.py` ngay 2026-10-12 21:00. Dung lam moc so sanh khi port Rust.
```json
{
"date": "2026-10-12 21:00",
"bundle": {
"dist/daw_engine": {
"mb": 146.6,
"files": 403
},
"src-tauri/resources/daw_engine": {
"mb": 1841.8,
"files": 4187
},
"has:torch": false,
"has:torchvision": false,
"has:sympy": false,
"has:networkx": false,
"has:tensorflow": false
},
"cold_start": {
"startup_ms": 5700.4,
"port": 8000,
"rss_mb": 68.3
},
"import": {
"import_app_main_ms": 730.9,
"subprocess_wall_ms": 822.0
},
"render_mixdown": {
"tracks": 4,
"seconds_each": 30.0,
"sr": 48000,
"runs_ms": [
1808.2,
515.9,
529.0
],
"min_ms": 515.9,
"median_ms": 529.0,
"realtime_factor": 0.0172
}
}
```
+8 -6
View File
@@ -128,7 +128,7 @@ Chia hệ thống thành 5 tầng, mỗi tầng có "hệ số port" khác nhau:
### 4.1 Việc tối ưu "miễn phí", làm được NGAY (không cần port)
1. **Xoá `torch` + `torchvision` khỏi bundle** (không được import ở đâu trong source) → −1,5 GB, giảm thời gian build/install ngay lập tức. Chỉ cần loại khỏi môi trường build / `engine.spec` và kiểm tra lại bằng test suite.
1. ✅ **Đã làm (P0)**: xoá `torch` + `torchvision` khỏi bundle → **1.842 MB → 146,6 MB**, không cần port. Chỉ cần thêm `excludes` trong `engine.spec`; đã kiểm chứng app không import torch (`tools/p0_baseline.py`, `md/BASELINE_P0.md`).
2. Vá `app/static/js/app.jsx` 37k dòng bằng bundler thật (esbuild/vite) thay vì Babel 1-file → `app.precompiled.js` nhỏ hơn, build nhanh hơn, có source map.
3. Tách `app.jsx` thành module (đang là 1 component khổng lồ) → giảm rủi ro regression, tiền đề bắt buộc nếu sau này port UI.
4. Thay relay `WebView → WS → Python → SHM → C++` bằng **Tauri IPC trực tiếp tới Rust → SHM** cho phần control: bỏ 1 hop mạng loopback, bỏ khả năng treo do uvicorn, giảm jitter điều khiển. Đây là bước rẻ nhất trong hướng T4.
@@ -170,11 +170,13 @@ Chia hệ thống thành 5 tầng, mỗi tầng có "hệ số port" khác nhau:
## 5. Kế hoạch triển khai theo giai đoạn
### P0 — Dọn dẹp trước khi port (1-3 ngày, không rủi ro)
- [ ] Xác nhận `torch`/`torchvision` không dùng → loại khỏi build, đo lại bundle (mục tiêu <400 MB ngay).
- [ ] Chốt "hợp đồng API": export OpenAPI JSON + danh sách message WS (`set_chain`, `set_param`, `flush`, `started`, `stats`…) làm đặc tả cho bản Rust.
- [ ] Dựng **golden test**: bộ WAV/JSON mẫu cho từng endpoint DSP; chạy trên Python hiện tại, lưu hash → đây là tiêu chuẩn đúng cho Rust.
- [ ] Đo baseline: thời gian start, RAM, thời gian scan plugin, thời gian render (ghi vào `md/`).
### P0 — Dọn dẹp trước khi port — ✅ ĐÃ XONG (2026-10-12)
- [x] Xác nhận `torch`/`torchvision` không dùng (block import torch → `scipy.signal` + mọi `app.core.*` import OK) → thêm exclude vào `engine.spec` (torch/torchvision/torchaudio/triton/sympy/networkx/tqdm/einops/optree/tensorflow/keras/jax).
**Kết quả: bundle engine 1.841,8 MB / 4.187 file → 146,6 MB / 403 file (−92%)**, đã thay cả bản ship trong `src-tauri/resources/daw_engine`.
- [x] Chốt "hợp đồng API": `md/api_contract/openapi.json` (86 path / 95 operation / 53 schema) + `md/api_contract/ws_protocol.md` (WS realtime + worklet).
- [x] Golden test: `tools/golden_conformance.py` → `md/api_contract/golden/manifest.json`, 10 case DSP (zero-crossing, micro-crossfade, peak/RMS waveform, cut/loop, multitrack mixdown, analyze, LUFS/true-peak/mono-side). Tái lập ổn định (`--check` = 0 mismatch giữa các tiến trình); mixdown hash theo mẫu audio vì header WAV không ổn định.
- [x] Đo baseline: `tools/p0_baseline.py` → `md/BASELINE_P0.md` (cold start **5.700 ms**, RSS **68 MB**, import `app.main` 731 ms, mixdown 4 track × 30 s: min **516 ms** = 0,017× realtime).
### P1 — Rust service khung, chạy song song (2-4 tuần)
- [ ] Crate `engine/` (axum): router, auth, health, `/api/v1/system`, `/tasks`.
+22
View File
@@ -0,0 +1,22 @@
{
"cases": {
"analyzer.analyze_audio": "20fb044c076bc180810a92edb0cf247f62b2dea5e16f769208b634deeb6f36ef",
"audio_editor.cut_and_loop_segment": "16848af659a11994904d4d08d3b91ba58224ce0f057cc950b645c0c3a8417226",
"audio_editor.mix_multitrack_session": "46dcd4da24a0765cc78707eb4079fc289cc1e23bc741ecd12309683c59ce15bc",
"dsp_utils.apply_micro_crossfade": "c43756937a0e5a2e285c9af1d8d848041033fbb3396057d8ca3bfc513bdca758",
"dsp_utils.find_zero_crossing": "8ee6913ca5066eb6b4949040e6cb0f3d47589802de67b886d905b2ea79049dba",
"dsp_utils.generate_peak_waveform": "1a629e10dd4207bb8faafee747eac4955b05d3e624236e0a13bb51bd0f2a5f82",
"dsp_utils.generate_rms_waveform": "6ecf9662cf77a6a3e8b193dfef9b32d32bdff14c355a8915cd8f81ee44b80f5b",
"loudness.integrated_lufs": "0914a33306e2832bba22f7bfed82a8c30c785752b3a55dab3bb5151354709972",
"loudness.mono_side_ratio": "6ab9135f88cfd6092ccd2c3f0eb3285debd7227f9675b7e8a6cfe86b473b3c0c",
"loudness.true_peak_db": "4726ed91ff71f4c2fd9f66bef790916cb7574bd715598af0ff146fa582520e56"
},
"env": {
"numpy": "1.26.4",
"python": "3.13.7",
"sample_rate": 48000,
"scipy": "1.17.1"
},
"generated_by": "tools/golden_conformance.py",
"schema": 1
}
File diff suppressed because it is too large Load Diff
+72
View File
@@ -0,0 +1,72 @@
# Hợp đồng realtime FX (WS + worklet) — SonicForgeStudio
Trích từ `app/api/v1/plugins.py`, `app/core/fx_realtime.py`,
`app/static/js/sf-fx-realtime.js`, `app/static/js/fx-rt-worker.js`, `app/static/js/app.jsx`
tại HEAD `7c236d4`. Đây là đặc tả để port sang Rust (P1/P4).
## 1. HTTP (REST)
| Method | Path | Body | Trả về |
|---|---|---|---|
| POST | `/api/v1/plugins/fx-realtime/start` | `{fx_chain: [], sample_rate: 44100}` | `{success, session_id, ws_url, block_size, sample_rate}` |
| POST | `/api/v1/plugins/fx-realtime/stop` | `{session_id}` | `{success, stopped}` |
| POST | `/api/v1/plugins/fx-realtime/set-chain` | `{session_id, fx_chain}` | `{success, updated}` |
| GET | `/api/v1/plugins/bridge/status` | — | trạng thái bridge/SHM |
| POST | `/api/v1/plugins/render` | job render offline (VST) | file kết quả |
| POST | `/api/v1/plugins/soundfont-render` | job render SF2 | WAV |
- `block_size` = **256 mẫu** (`FXRT_BLOCK` trong `FxRealtimeIPC.h`).
- Auth: Bearer token hoặc `?token=`; desktop loopback không token → auto admin.
## 2. WebSocket audio
`GET /api/v1/plugins/ws/fx-realtime/{session_id}?token=...`
| Hướng | Kiểu frame | Nội dung |
|---|---|---|
| client → server | **binary** | 2048 B = 256 frame × 2 kênh `float32` interleaved |
| server → client | **binary** | như trên, đã qua chain FX |
| client → server | text JSON | `{"cmd":"set_param","slot":n,"key":"...","value":f}` · `{"cmd":"latency","slot":n,"samples":n}` · `{"cmd":"flush"}` |
| server → client | text JSON | `{"cmd":"latency","slot":n,"samples":n}` · `{"cmd":"flush_done"}` |
Server: `ws_fx_realtime()` (plugins.py ~1344) — vòng lặp receive block → SHM ring →
`fx_vst_bridge --realtime-fx` (hoặc `--juce-fx`) → SHM → send.
## 3. Worklet ↔ main thread (MessagePort)
`sf-fx-realtime.js` (AudioWorkletProcessor `sf-fx-realtime`).
Hằng số: `BLOCK=256`, `BURST=4` (gom 4 block = 1024 mẫu/frame), `FILL=40` (≈213 ms @48k),
`RESUME_FILL=24`, `CAP=64` (≈341 ms), `FADE=512`, `HOLD_FADE=4`, `WRAP_FADE=256`.
| type | Hướng | Payload |
|---|---|---|
| `__direct` | main → worklet | chuyển `MessagePort` (worker↔worklet trực tiếp, bỏ main thread) |
| `in` | worklet → main/worker | `{data: ArrayBuffer}` 1024 mẫu stereo interleaved |
| `out` | worker → worklet | `{l, r: Float32Array}` block đã xử lý |
| `started` | worklet → main | phát block đầu tiên (đã đủ FILL) |
| `clear_queue` | main → worklet | `{mode: 'play'\|'seek'\|'loop'}` — wipe queue, `hasStarted=false` (mode `play`) |
| `flush` | worklet → main | yêu cầu engine xả ring stale |
| `flush_done` | worker → worklet | engine đã flush |
| `flush_for_stop` | main → worklet | dừng phát (giữ queue) |
| `set_param` | worklet → main | forward nguyên văn tới WS |
| `latency` | main → worklet | `{samples}` — latency chain thật |
| `stats` | worklet → main | `{underruns, holds, fades, silences, capDrops, fadeIns, queueLen}` (≈2 s/lần) |
| `dbg` | worklet → main | `{tag, t, ...}`: `fx_in_first`, `fx_out_first`, `fx_play_first`, `fx_seek_first`, `fx_loop_clear`, `fx_seek_clear`, `fx_wrap_xfade` |
| `dbg_state` | main → worklet | yêu cầu snapshot trạng thái nội bộ |
| `dbg_state_r` | worklet → main | `{k, t, q, playing, hasStarted, silenced, flushPending, stopped, wrapMode, wrapFadeBlocks, latencySamples, resumeFill, inCount, outPos, fadeLeft, holdFadeLeft}` |
## 4. Đồng bộ playhead / PDC (phía UI, app.jsx)
- `FX_RT_WET_LAG_SAMPLES = 10240` (= FILL 40 × 256) — độ trễ nội dung khi vào FX path.
- `pdcDelay.delayTime` = 10240/sr khi FX active, 0 khi dry (`fxRtApplyPdc`).
- Crossfade dry↔wet: `masterFxDryGain` / `masterFxGain` rampe `setTargetAtTime(tc=0.025)`.
- `fxRt.crossfade(token)` bất biến: `dryGain + fxGain = 1`.
- Warm bypass: giữ session/node/ws khi PWR OFF giữa lúc play (`fxRtWarmPlayHold`).
## 5. Ghi chú cho bản Rust (P4)
Hợp đồng tối thiểu cần giữ để frontend không phải sửa:
1. `POST /fx-realtime/start` → `session_id` + `ws_url`, `block_size=256`.
2. WS binary 2048 B/bản tin, text JSON 3 lệnh (`set_param`, `latency`, `flush`) + 2 phản hồi (`latency`, `flush_done`).
3. Thay hop Python bằng Rust: `worklet → Tauri IPC/Rust → SHM`, giữ nguyên SHM layout với `fx_vst_bridge`.
+193
View File
@@ -0,0 +1,193 @@
"""P0 golden conformance harness (Python oracle) - SonicForgeStudio.
Muc dich: sinh manifest hash cho cac ham DSP thuan (deterministic) lam CHUAN
DOI CHIEU khi port sang Rust (P1-P3). Chay:
python tools/golden_conformance.py --write # ghi manifest
python tools/golden_conformance.py --check # so voi manifest da ghi
Moi case tra ve bytes canonical (so thuc lam tron 6 chu so thap phan) -> sha256.
Cung input (fixture sinh bang seed co dinh trong code) + cung thuat toan =>
cung hash; lech => in ro case nao.
"""
import argparse
import hashlib
import io
import json
import os
import sys
import tempfile
import numpy as np
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, ROOT)
MANIFEST = os.path.join(ROOT, "md", "api_contract", "golden", "manifest.json")
SR = 48000
# ── fixtures (seed co dinh, khong dung file ngoai) ───────────────────────────
def _tone(freq, dur, sr=SR, amp=0.5):
t = np.linspace(0, dur, int(sr * dur), endpoint=False)
return (amp * np.sin(2 * np.pi * freq * t)).astype(np.float32)
def _fixture_stereo():
rng = np.random.default_rng(1234)
n = SR * 2
t = np.arange(n) / SR
left = (0.4 * np.sin(2 * np.pi * 220 * t) + 0.05 * rng.standard_normal(n)).astype(np.float32)
right = (0.4 * np.sin(2 * np.pi * 330 * t + 0.7) + 0.05 * rng.standard_normal(n)).astype(np.float32)
return np.column_stack([left, right]).astype(np.float32)
def _fixture_click_track():
"""Click track 120 BPM de analyzer/beat khong bi suy bien."""
n = SR * 4
y = np.zeros(n, dtype=np.float32)
step = SR // 2 # 120 BPM
for i in range(0, n, step):
y[i:i + 64] = 0.8
return y
def _write_wav(dirpath, name, data, sr=SR):
import soundfile as sf
p = os.path.join(dirpath, name)
sf.write(p, data, sr, subtype="FLOAT")
return p
# ── canonical hoa + hash ─────────────────────────────────────────────────────
def _canon(obj):
if isinstance(obj, np.ndarray):
a = np.asarray(obj, dtype=np.float64)
return json.dumps({"dtype": str(obj.dtype), "shape": list(a.shape),
"data": np.round(a.ravel(), 6).tolist()},
sort_keys=True)
if isinstance(obj, dict):
return json.dumps({k: _canon(v) for k, v in sorted(obj.items())}, sort_keys=True)
if isinstance(obj, (list, tuple)):
return json.dumps([_canon(v) for v in obj])
if isinstance(obj, (np.floating, float)):
return json.dumps(round(float(obj), 6))
if isinstance(obj, (np.integer, int)):
return json.dumps(int(obj))
if isinstance(obj, bytes):
return json.dumps(list(obj))
return json.dumps(obj, sort_keys=True)
def _sha(obj):
return hashlib.sha256(_canon(obj).encode("utf-8")).hexdigest()
# ── cac case ─────────────────────────────────────────────────────────────────
def collect():
cases = {}
import soundfile as sf
tmp = tempfile.mkdtemp(prefix="sf_golden_")
stereo = _fixture_stereo()
mono = _tone(440.0, 1.0)
p_stereo = _write_wav(tmp, "stereo.wav", stereo)
p_mono = _write_wav(tmp, "mono.wav", mono)
p_click = _write_wav(tmp, "click.wav", _fixture_click_track())
from app.core import dsp_utils, audio_editor, analyzer, loudness
# dsp_utils
targets = [0.0, 0.123, 0.5, 1.37, 1.999]
cases["dsp_utils.find_zero_crossing"] = [
dsp_utils.find_zero_crossing(stereo[:, 0], SR, tt) for tt in targets]
orig = stereo[:, 0].copy()
edited = np.roll(orig, 37)
cases["dsp_utils.apply_micro_crossfade"] = dsp_utils.apply_micro_crossfade(
orig, edited, start_sample=SR // 2, fade_len_ms=10, sr=SR)
cases["dsp_utils.generate_peak_waveform"] = dsp_utils.generate_peak_waveform(p_stereo, 400)
cases["dsp_utils.generate_rms_waveform"] = dsp_utils.generate_rms_waveform(p_stereo, 400)
# audio_editor
seg = audio_editor.cut_and_loop_segment(
p_stereo, 0.25, 0.75, loop_count=2, fade_in_ms=20, fade_out_ms=20,
volume_db_change=-3.0)
cases["audio_editor.cut_and_loop_segment"] = {
"channels": seg.channels, "frame_rate": seg.frame_rate,
"sample_width": seg.sample_width, "raw": seg.raw_data}
# bit_depth=32 (IEEE float) — KHONG dither, bit-exact. 16/24-bit dung TPDF
# dither ngau nhien (np.random.default_rng()) nen output KHONG tai lap duoc.
out_wav = os.path.join(tmp, "mix.wav")
tracks_meta = [
{"file_path": p_stereo, "volume": 0.8, "muted": False},
{"file_path": p_mono, "volume": 0.6, "muted": False},
{"file_path": p_click, "volume": 0.4, "muted": True},
]
audio_editor.mix_multitrack_session(tracks_meta, out_wav, sample_rate=SR, bit_depth=32)
# Hash theo MAU audio (khong hash byte file): header WAV do pydub/ffmpeg ghi
# co metadata khong on dinh giua cac lan chay, du samples giong het nhau.
y_mix, sr_mix = sf.read(out_wav, dtype="float64")
cases["audio_editor.mix_multitrack_session"] = {"sample_rate": sr_mix, "audio": y_mix}
# analyzer
cases["analyzer.analyze_audio"] = analyzer.analyze_audio(p_click)
# loudness
cases["loudness.integrated_lufs"] = loudness.integrated_lufs(stereo, SR)
cases["loudness.true_peak_db"] = loudness.true_peak_db(stereo, SR)
cases["loudness.mono_side_ratio"] = loudness.mono_side_ratio(stereo)
return cases
def build_manifest():
import numpy as np
import scipy
cases = collect()
return {
"schema": 1,
"generated_by": "tools/golden_conformance.py",
"env": {"python": sys.version.split()[0], "numpy": np.__version__,
"scipy": scipy.__version__, "sample_rate": SR},
"cases": {k: _sha(v) for k, v in cases.items()},
}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--write", action="store_true")
ap.add_argument("--check", action="store_true")
args = ap.parse_args()
manifest = build_manifest()
if args.check:
with io.open(MANIFEST, "r", encoding="utf-8") as f:
ref = json.load(f)
bad = []
for k, h in ref["cases"].items():
cur = manifest["cases"].get(k)
if cur != h:
bad.append(k)
extra = [k for k in manifest["cases"] if k not in ref["cases"]]
for k in bad:
print("MISMATCH %s\n ref=%s\n cur=%s" % (k, ref["cases"][k], manifest["cases"][k]))
for k in extra:
print("NEW-CASE %s" % k)
print("cases=%d mismatch=%d new=%d" % (len(ref["cases"]), len(bad), len(extra)))
return 1 if bad else 0
os.makedirs(os.path.dirname(MANIFEST), exist_ok=True)
with io.open(MANIFEST, "w", encoding="utf-8", newline="\n") as f:
json.dump(manifest, f, indent=1, sort_keys=True)
print("WROTE %s (%d cases)" % (MANIFEST, len(manifest["cases"])))
for k, h in sorted(manifest["cases"].items()):
print(" %-45s %s" % (k, h[:16]))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+168
View File
@@ -0,0 +1,168 @@
"""P0 baseline metrics - SonicForgeStudio (do truoc khi port Rust).
Do: kich thuoc bundle, thoi gian cold start + RAM cua daw_engine.exe,
thoi gian import app.main, thoi gian mixdown 4 track 30s.
Chay: python tools/p0_baseline.py # in ra stdout + ghi md/BASELINE_P0.md
python tools/p0_baseline.py --print # chi in
"""
import argparse
import io
import json
import os
import shutil
import statistics
import subprocess
import sys
import tempfile
import time
import urllib.request
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
ENGINE_EXE = os.path.join(ROOT, "dist", "daw_engine", "daw_engine.exe")
ENGINE_DIRS = ["dist/daw_engine", "src-tauri/resources/daw_engine"]
OUT_MD = os.path.join(ROOT, "md", "BASELINE_P0.md")
SR = 48000
def dir_size_mb(rel):
p = os.path.join(ROOT, rel)
if not os.path.exists(p):
return None
total = files = 0
for r, _d, fs in os.walk(p):
for f in fs:
try:
total += os.path.getsize(os.path.join(r, f))
files += 1
except OSError:
pass
return {"mb": round(total / 1048576.0, 1), "files": files}
def measure_bundle():
out = {}
for d in ENGINE_DIRS:
s = dir_size_mb(d)
if s:
out[d] = s
for sub in ("torch", "torchvision", "sympy", "networkx", "tensorflow"):
p = os.path.join(ROOT, "dist", "daw_engine", "_internal", sub)
out["has:" + sub] = os.path.exists(p)
return out
def _tasklist_mem_kb(pid):
try:
r = subprocess.run(["tasklist", "/FI", "PID eq %d" % pid, "/FO", "CSV", "/NH"],
capture_output=True, text=True, timeout=15)
row = r.stdout.strip().splitlines()[0]
cells = [c.strip('"') for c in row.split('","')]
return int(cells[-1].replace(",", "").replace(" K", "").strip())
except Exception:
return None
def measure_cold_start(timeout_s=90):
if not os.path.exists(ENGINE_EXE):
return {"error": "engine exe khong ton tai: " + ENGINE_EXE}
env = dict(os.environ)
env["SF_PORT"] = "8000"
t0 = time.perf_counter()
proc = subprocess.Popen([ENGINE_EXE], cwd=os.path.dirname(ENGINE_EXE), env=env,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
port = None
try:
while time.perf_counter() - t0 < timeout_s:
if proc.poll() is not None:
return {"error": "engine thoat som rc=%s" % proc.returncode}
for p in range(8000, 8011):
try:
with urllib.request.urlopen("http://127.0.0.1:%d/health" % p, timeout=0.5) as r:
if r.status == 200:
port = p
break
except Exception:
continue
if port:
break
time.sleep(0.05)
ms = round((time.perf_counter() - t0) * 1000.0, 1)
mem = _tasklist_mem_kb(proc.pid)
result = {"startup_ms": ms, "port": port, "rss_mb": round(mem / 1024.0, 1) if mem else None}
finally:
subprocess.run(["taskkill", "/PID", str(proc.pid), "/T", "/F"],
capture_output=True, text=True)
try:
proc.wait(timeout=10)
except Exception:
pass
return result
def measure_import_time():
code = ("import time,sys;t=time.perf_counter();import app.main;"
"print(round((time.perf_counter()-t)*1000,1))")
t0 = time.perf_counter()
r = subprocess.run([sys.executable, "-c", code], cwd=ROOT, capture_output=True, text=True)
wall = round((time.perf_counter() - t0) * 1000.0, 1)
try:
imp = float(r.stdout.strip().splitlines()[-1])
except Exception:
imp = None
return {"import_app_main_ms": imp, "subprocess_wall_ms": wall}
def measure_render(reps=3):
import numpy as np
sys.path.insert(0, ROOT)
from app.core import audio_editor
tmp = tempfile.mkdtemp(prefix="sf_bench_")
dur = 30.0
n = int(SR * dur)
t = np.arange(n) / SR
paths = []
for i, f in enumerate((110.0, 220.0, 330.0, 440.0)):
y = (0.3 * np.sin(2 * np.pi * f * t)).astype(np.float32)
p = os.path.join(tmp, "t%d.wav" % i)
import soundfile as sf
sf.write(p, np.column_stack([y, y]), SR, subtype="FLOAT")
paths.append(p)
meta = [{"file_path": p, "volume": 0.8, "muted": False} for p in paths]
times = []
for i in range(reps):
out = os.path.join(tmp, "mix%d.wav" % i)
t0 = time.perf_counter()
audio_editor.mix_multitrack_session(meta, out, sample_rate=SR, bit_depth=32)
times.append(round((time.perf_counter() - t0) * 1000.0, 1))
shutil.rmtree(tmp, ignore_errors=True)
return {"tracks": 4, "seconds_each": dur, "sr": SR,
"runs_ms": times, "min_ms": min(times), "median_ms": round(statistics.median(times), 1),
"realtime_factor": round(min(times) / (dur * 1000.0), 4)}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--print", action="store_true")
args = ap.parse_args()
metrics = {
"date": time.strftime("%Y-%m-%d %H:%M"),
"bundle": measure_bundle(),
"cold_start": measure_cold_start(),
"import": measure_import_time(),
"render_mixdown": measure_render(),
}
text = "# Baseline P0 - SonicForgeStudio (Python engine)\n\n" \
"Sinh boi `tools/p0_baseline.py` ngay %s. Dung lam moc so sanh khi port Rust.\n\n```json\n%s\n```\n" % (
metrics["date"], json.dumps(metrics, indent=1, ensure_ascii=False))
print(text)
if not args.print:
os.makedirs(os.path.dirname(OUT_MD), exist_ok=True)
with io.open(OUT_MD, "w", encoding="utf-8", newline="\n") as f:
f.write(text)
print("WROTE", OUT_MD)
return 0
if __name__ == "__main__":
raise SystemExit(main())