Files

276 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Review DAW-standard — TASKS_DAW_A.md §49-57 (R1-R8), cài đặt thực tế.
Mọi test chạy qua DAW THẬT: PythonRenderEngine.render_project (RENDER_ENGINE=
bridge mặc định) + native_bridge fx_vst_bridge.exe --render-fx + project JSON
đầy đủ (AUDIO_ITEM track, master, mastering_settings.chain). API smoke qua
TestClient(app) + /api/v1/plugins/render (auth admin) — chính là luồng frontend
Export.
- R1: track insert chain [eq, vst3-adelay, limiter] — impulse → peak tại
44100 (delay 1s), im lặng trước: vst3 ở giữa chain, không gom cuối.
- R2: gain/pan sau track chain — rebalance mid-only (giết side) + pan 0.5 →
output L≠R với tỉ lệ constant-power: pan phải chạy SAU chain.
- R3: master chain serial trên mix tổng — mastering_settings.chain
[eq, vst3-adelay, limiter] → impulse peak tại 44100.
- R4: master fader cuối (đã có test_55_fader_after_master_chain).
- R5: live=export (đã có test_53_live_equals_export).
- R6: PDC (đã có test_56_pdc_aligns_tracks).
- R7: gain staging — track fader -6 / master fader -6 / chain +6 / fader -6
net 0: dB cộng dồn đúng qua từng stage.
- R8: mono/stereo — mono source pad L==R giữ qua chain vst3; stereo source
giữ separation qua chain builtin.
- R-API: TestClient + POST /api/v1/plugins/render project thật (track chain +
master chain) → WAV peak tại 44100.
"""
import os
import sys
import wave
import numpy as np
import pytest
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, REPO)
sys.path.insert(0, os.path.dirname(__file__))
import test_daw_phase5 as p5 # noqa: E402 (dùng chung helper/fixture)
SR = p5.SR
_write_wav = p5._write_wav
_read_wav = p5._read_wav
_snr_db = p5._snr_db
ADELAY = p5.ADELAY
FX_BRIDGE = p5.FX_BRIDGE
@pytest.fixture(scope="module")
def fx_bridge():
if not os.path.isfile(FX_BRIDGE):
pytest.skip("fx_vst_bridge.exe chưa build (native_bridge/build/Release)")
return FX_BRIDGE
@pytest.fixture(scope="module")
def adelay():
if not os.path.isfile(os.path.join(ADELAY, "Contents", "x86_64-win", "adelay.vst3")):
pytest.skip("adelay.vst3 sample chưa build (native_bridge/build/VST3/Release)")
return ADELAY
@pytest.fixture
def bridge_env(fx_bridge, monkeypatch):
"""Engine render_project → native_render.render_fx_chain → fx_vst_bridge.exe
(tìm qua SF_FX_BRIDGE_PATH)."""
monkeypatch.setenv("SF_FX_BRIDGE_PATH", fx_bridge)
return fx_bridge
# ── helpers ────────────────────────────────────────────────────────────────
def _audio_track(wav_path, vol=0.0, pan=0.0, fx_chain=None, track_id="t1"):
return {
"id": track_id, "type": "AUDIO",
"volume_db": vol, "pan": pan, "mute": False,
"fx_chain": fx_chain or [], "vst_fx_chain": [], "vst_fx_bypass": False,
"fx_active": True,
"items": [{
"type": "AUDIO_ITEM", "start_bar": 0.0, "duration_bars": 1.0,
"clip_start_offset_bars": 0.0,
"source_data": {"audio_file_url": wav_path},
}],
}
def _project(tracks, master_vol=0.0, master_chain=None, ms_chain=None,
length_bars=1.0):
master = {"bypass": False, "volume_db": master_vol}
if master_chain is not None:
master["fx_chain"] = master_chain
proj = {
"metadata": {"bpm": 120.0, "time_signature_numerator": 4},
"main_session": {"length_bars": length_bars, "tracks": tracks, "master": master},
"section_store": {},
}
if ms_chain is not None:
proj["mastering_settings"] = {"chain": ms_chain}
return proj
def _impulse_wav(path, amp=1.0, n=2 * SR):
audio = np.zeros((2, n), dtype=np.float32)
audio[0, 0] = amp
audio[1, 0] = amp
_write_wav(path, audio, SR)
def _render_project(project, out):
from app.core.render_engine import PythonRenderEngine
engine = PythonRenderEngine()
engine.render_project(project, out)
return _read_wav(out)[0]
def _peak_idx(data):
return int(np.unravel_index(np.argmax(np.abs(data)), data.shape)[1])
# ── R1 Track insert chain serial ───────────────────────────────────────────
def test_r1_track_chain_order_impulse(bridge_env, adelay, tmp_path):
"""Track chain [eq, vst3-adelay, limiter] qua render_project thật — impulse
peak tại ≈44100, im lặng trước: vst3 ở giữa chain (sau EQ, trước limiter)."""
chain = [
{"type": "builtin", "id": "eq",
"params": {"g1": 3.0, "g2": -2.0, "g3": 1.0, "g4": 0.5}},
{"type": "vst3", "path": adelay},
{"type": "builtin", "id": "limiter", "params": {"ceiling": -3.0}},
]
wav = str(tmp_path / "imp.wav")
_impulse_wav(wav)
out = str(tmp_path / "r1.wav")
data = _render_project(_project([_audio_track(wav, fx_chain=chain)]), out)
idx = _peak_idx(data)
assert abs(idx - SR) < 200, f"peak tại {idx} — mong đợi ~{SR} (delay 1s)"
assert float(np.max(np.abs(data[:, :int(SR * 0.9)]))) == 0.0, "có tín hiệu trước delay"
# ── R2 Gain/pan sau track chain ────────────────────────────────────────────
def test_r2_gain_pan_after_track_chain(bridge_env, tmp_path):
"""Chain rebalance mid-only (giết side) → L==R. Pan 0.5 SAU chain tạo lại
side (L≠R, constant-power R/L=sin/cos≈2.414). Nếu pan chạy trước chain,
rebalance sẽ mono lại → L==R (fail)."""
chain = [{"type": "builtin", "id": "rebalance",
"params": {"mid": 0.0, "side": -120.0}}]
wav = str(tmp_path / "imp.wav")
_impulse_wav(wav, amp=0.8)
d0 = _render_project(_project([_audio_track(wav, pan=0.0, fx_chain=chain)]),
str(tmp_path / "pan0.wav"))
assert _snr_db(d0[0:1], d0[1:2]) >= 60.0, "rebalance mid-only phải ra L==R"
d5 = _render_project(_project([_audio_track(wav, pan=0.5, fx_chain=chain)]),
str(tmp_path / "pan5.wav"))
lr = float(np.max(np.abs(d5[1]))) / float(np.max(np.abs(d5[0])))
assert 1.8 < lr < 3.2, f"pan không SAU chain: R/L={lr:.3f} (mong đợi 2.414)"
# gain cũng sau chain: chain rebalance mid-only không đổi amp → pan=0,
# vol -6 → đỉnh = base/2 (gain scale đúng ở stage riêng)
d6 = _render_project(_project([_audio_track(wav, pan=0.0, vol=-6.0, fx_chain=chain)]),
str(tmp_path / "vol6.wav"))
assert abs(float(np.max(np.abs(d6))) - 0.4) < 0.03, "vol_db -6 sau chain sai"
# ── R3 Master chain serial trên mix tổng ───────────────────────────────────
def test_r3_master_chain_serial_on_mix(bridge_env, adelay, tmp_path):
"""mastering_settings.chain [eq, vst3-adelay, limiter] trên mix tổng —
impulse peak tại ≈44100 (vst3 delay giữa chain master, không gom cuối)."""
chain = [
{"type": "eq", "params": {"g1": 3.0, "g2": -2.0, "g3": 1.0, "g4": 0.5}},
{"type": "vst3", "path": adelay},
{"type": "limiter", "params": {"ceiling": -3.0}},
]
wav = str(tmp_path / "imp.wav")
_impulse_wav(wav)
out = str(tmp_path / "r3.wav")
data = _render_project(_project([_audio_track(wav)], ms_chain=chain), out)
idx = _peak_idx(data)
assert abs(idx - SR) < 200, f"peak tại {idx} — mong đợi ~{SR}"
assert float(np.max(np.abs(data[:, :int(SR * 0.9)]))) == 0.0, "có tín hiệu trước delay"
# ── R7 Gain staging ────────────────────────────────────────────────────────
def test_r7_gain_staging(bridge_env, tmp_path):
"""dB cộng dồn qua từng stage: track fader -6 / master fader -6 / master
chain +6 / fader -6 — net 0 → bằng base; net -6 → base/2."""
t = np.arange(SR // 2) / SR
sig = (0.3 * np.sin(2 * np.pi * 440 * t)).astype(np.float32)
wav = str(tmp_path / "sig.wav")
_write_wav(wav, np.stack([sig, sig]), SR)
GAIN6 = [{"type": "builtin", "id": "gain", "params": {"db": 6.0}}]
def peak(vol=0.0, mvol=0.0, mchain=None):
out = str(tmp_path / f"g_{vol}_{mvol}_{bool(mchain)}.wav")
d = _render_project(_project([_audio_track(wav, vol=vol)],
master_vol=mvol, master_chain=mchain), out)
return float(np.max(np.abs(d)))
base = peak()
assert base > 0.1
# net -6 dB → base/2
for name, v in [("track fader -6", peak(vol=-6.0)),
("master fader -6", peak(mvol=-6.0)),
("track-6 + chain+6 + fader-6", peak(vol=-6.0, mvol=-6.0, mchain=GAIN6))]:
assert abs(v - base / 2) < 0.06 * base, \
f"{name} staging sai: {v:.4f} vs base/2 {base / 2:.4f}"
# net 0 dB → base
for name, v in [("track-6 + chain+6", peak(vol=-6.0, mchain=GAIN6)),
("chain+6 + fader-6", peak(mvol=-6.0, mchain=GAIN6))]:
assert abs(v - base) < 0.06 * base, f"{name} staging sai: {v:.4f} vs base {base:.4f}"
# ── R8 Mono/Stereo ─────────────────────────────────────────────────────────
def test_r8_mono_stereo(bridge_env, adelay, tmp_path):
"""Mono source → pad L==R, giữ qua chain vst3 (delay 1s). Stereo source
L≠R → giữ separation qua chain builtin (xử lý per-channel đúng)."""
mono = np.zeros(2 * SR, dtype=np.float32)
mono[0] = 1.0
wp = str(tmp_path / "mono.wav")
with wave.open(wp, "wb") as w:
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(SR)
w.writeframes((mono * 32767).astype(np.int16).tobytes())
d = _render_project(_project([_audio_track(wp, fx_chain=[{"type": "vst3", "path": adelay}])]),
str(tmp_path / "mono_out.wav"))
assert _snr_db(d[0:1], d[1:2]) >= 60.0, "mono pad L==R không giữ qua chain vst3"
assert abs(_peak_idx(d) - SR) < 200, "mono delay qua chain sai vị trí"
st = np.zeros((2, 2 * SR), dtype=np.float32)
st[0, 0] = 1.0
st[1, 0] = 0.4
wp2 = str(tmp_path / "st.wav")
_write_wav(wp2, st, SR)
eq = [{"type": "builtin", "id": "eq", "params": {"g1": 3.0}}]
d2 = _render_project(_project([_audio_track(wp2, fx_chain=eq)]),
str(tmp_path / "st_out.wav"))
ratio = float(np.max(np.abs(d2[0]))) / float(np.max(np.abs(d2[1])))
assert 1.8 < ratio < 3.2, f"stereo separation không giữ qua chain: L/R={ratio:.3f}"
# ── R-API: cài đặt thực tế — TestClient + /render ──────────────────────────
def test_r_api_render_real_project(bridge_env, adelay, tmp_path):
"""Luồng Export thật: TestClient(app), login admin, GET /capabilities, POST
/api/v1/plugins/render với project thật (track chain + master chain đều có
vst3 adelay, project 2 bars) → impulse qua 2 delay 1s → peak tại ≈88200
(2×SR) — 2 chain nối tiếp không mất tín hiệu."""
from test_plugin_api import client, get_admin_token
token = get_admin_token()
assert token, "admin login fail"
h = {"Authorization": f"Bearer {token}"}
r = client.get("/api/v1/system/capabilities")
assert r.status_code == 200, r.text[:300]
wav = str(tmp_path / "imp.wav")
_impulse_wav(wav, n=3 * SR)
chain = [
{"type": "builtin", "id": "eq", "params": {"g1": 3.0}},
{"type": "vst3", "path": adelay},
{"type": "builtin", "id": "limiter", "params": {"ceiling": -3.0}},
]
proj = _project([_audio_track(wav, fx_chain=chain)], ms_chain=chain,
length_bars=2.0)
r = client.post("/api/v1/plugins/render", headers=h,
json={"project_json": proj, "output_filename": "review_api_out.wav"})
assert r.status_code == 200, f"render fail: {r.text[:400]}"
body = r.json()
path = body.get("path")
assert path and os.path.isfile(path), f"no output wav: {body}"
data = _read_wav(path)[0]
idx = _peak_idx(data)
assert abs(idx - 2 * SR) < 300, f"peak tại {idx} — mong đợi ~{2 * SR} (track 1s + master 1s)"
assert float(np.max(np.abs(data[:, :int(0.9 * SR)]))) == 0.0, "có tín hiệu trước delay"