Gap 13: BWF/INFO metadata cho WAV export — wav_bwf.patch_bwf_metadata chen bext + LIST/INFO (INAM/IART/ISRC/ICMT) truoc data chunk, giu nguyen PCM bits; render_project(metadata=) + RenderRequest.metadata + cloud render task; ExportModal Title/Artist/ISRC; test_66 (178 passed)

This commit is contained in:
2026-08-23 09:11:18 +07:00
parent 94a0bc7401
commit 1d4ff1282b
9 changed files with 222 additions and 10 deletions
+79
View File
@@ -857,3 +857,82 @@ def test_65_master_volume_automation(monkeypatch, tmp_path, fx_bridge):
assert env0 == pytest.approx(base0, rel=0.05), f"đoạn 0dB phải giữ nguyên: {env0} vs {base0}"
ratio = env1 / max(env0, 1e-9)
assert 0.20 < ratio < 0.30, f"envelope -12dB → gain ~0.25: {ratio}"
def test_66_bwf_metadata(monkeypatch, tmp_path):
"""Gap 13: render_project(metadata=...) chen bext + LIST/INFO vao WAV —
title/artist/isrc doc lai duoc, PCM bytes khong doi."""
monkeypatch.delenv("SF_BRIDGE_PATH", raising=False)
from app.core.render_engine import PythonRenderEngine
engine = PythonRenderEngine()
def project():
track = _midi_track()
track["volume_db"] = -12.0
return {
"metadata": {"bpm": 120.0, "time_signature_numerator": 4},
"main_session": {
"length_bars": 1.0,
"tracks": [track],
"master": {"fx_chain": [], "bypass": False, "volume_db": 0.0},
},
"section_store": {},
}
def chunks(path):
with open(path, "rb") as f:
data = f.read()
assert data[:4] == b"RIFF" and data[8:12] == b"WAVE"
out = {}
off = 12
while off + 8 <= len(data):
cid = data[off:off + 4]
size = int.from_bytes(data[off + 4:off + 8], "little")
body = data[off + 8:off + 8 + size]
if cid == b"data":
out[cid] = body
break
out[cid] = body
off += 8 + size + (size % 2)
return out
out_plain = str(tmp_path / "plain.wav")
out_meta = str(tmp_path / "meta.wav")
engine.render_project(project(), out_plain)
engine.render_project(project(), out_meta, metadata={
"title": "Test Song",
"artist": "Penguin",
"isrc": "US-ABC-26-00001",
"comment": "Gap 13 check",
"description": "BWF description",
"originator": "SonicForgeStudio",
"originator_reference": "proj-123",
})
# Khong metadata → khong bext/LIST (backward compatible)
cp = chunks(out_plain)
assert b"bext" not in cp and b"LIST" not in cp
cm = chunks(out_meta)
assert b"bext" in cm and b"LIST" in cm
# bext: description (0:256) + originator (256:288) + originator_reference (288:320)
bext = cm[b"bext"]
assert bext[0:256].split(b"\x00")[0] == b"BWF description"
assert bext[256:288].split(b"\x00")[0] == b"SonicForgeStudio"
assert bext[288:320].split(b"\x00")[0] == b"proj-123"
# INFO: INAM/IART/ISRC subchunks
info = cm[b"LIST"]
assert info[:4] == b"INFO"
subs = {}
off = 4
while off + 8 <= len(info):
sid = info[off:off + 4]
size = int.from_bytes(info[off + 4:off + 8], "little")
subs[sid] = info[off + 8:off + 8 + size].rstrip(b"\x00")
off += 8 + size + (size % 2)
assert subs[b"INAM"] == b"Test Song"
assert subs[b"IART"] == b"Penguin"
assert subs[b"ISRC"] == b"US-ABC-26-00001"
# PCM giong nhau (metadata khong lam doi audio bits)
d_plain, sr_plain = _read_wav(out_plain)
d_meta, sr_meta = _read_wav(out_meta)
assert sr_meta == sr_plain
assert np.allclose(d_meta, d_plain, atol=0.0, rtol=0.0)