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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user