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
+11 -1
View File
@@ -839,7 +839,9 @@ class PythonRenderEngine:
def render_project(self, project_json: dict, output_filepath: str,
bit_depth: int = 32, out_sample_rate: int = 0,
dither: bool = False, normalize: bool = False,
normalize_target: str = "-1dbtp"):
normalize_target: str = "-1dbtp",
metadata: dict = None):
# Gap 13: BWF/INFO metadata — tham số phụ, không đổi hành vi cũ khi None.
with RENDER_LOCK:
bpm = project_json["metadata"]["bpm"]
time_sig_num = project_json["metadata"].get("time_signature_numerator", 4)
@@ -963,4 +965,12 @@ class PythonRenderEngine:
out_sr, subtype="PCM_24")
else:
sf.write(output_filepath, master_buffer.T, out_sr, subtype="FLOAT")
# Gap 13: BWF/INFO chunks (bext + LIST/INFO title/artist/ISRC) —
# chen sau khi ghi PCM, loi metadata khong lam hong file audio.
if metadata:
try:
from app.core.wav_bwf import patch_bwf_metadata
patch_bwf_metadata(output_filepath, metadata)
except Exception as e:
logger.warning("[RenderEngine] BWF metadata skipped: %s", e)
return output_filepath
+88
View File
@@ -0,0 +1,88 @@
"""Gap 13: BWF/INFO metadata chunks cho WAV export (EBU Tech 3285 + RIFF INFO).
sf.write chi ghi fmt/data — ham nay chen bext (602 bytes fixed + coding history)
va LIST/INFO (INAM title, IART artist, ISRC, ICMT comment) ngay truoc data chunk
roi ghi lai file. Pure stdlib (struct/datetime).
"""
import datetime
import struct
_BEXT_FIXED = 602
def _pad(text, length):
raw = text.encode("utf-8", errors="replace")
return (raw[:length] + b"\x00" * length)[:length]
def _chunk(fourcc, payload):
pad = b"\x00" if len(payload) % 2 else b""
return fourcc + struct.pack("<I", len(payload)) + payload + pad
def _info_sub(fourcc, text):
if not text:
return b""
return _chunk(fourcc, _pad(text, len(text.encode("utf-8", errors="replace")) + 1))
def _bext(md, now):
fixed = bytearray(_BEXT_FIXED)
fixed[0:256] = _pad(md.get("description", ""), 256)
fixed[256:288] = _pad(md.get("originator", "SonicForgeStudio"), 32)
fixed[288:320] = _pad(md.get("originator_reference", ""), 32)
fixed[320:330] = _pad((md.get("date") or now.strftime("%Y-%m-%d")), 10)
fixed[330:338] = _pad((md.get("time") or now.strftime("%H:%M:%S")), 8)
struct.pack_into("<II", fixed, 338, 0, 0) # time reference low/high
struct.pack_into("<H", fixed, 346, 1) # version = 1
fixed[348:412] = bytes(64) # UMID (zero)
struct.pack_into("<hhhhh", fixed, 412, -32768, -32768, -32768, -32768, -32768) # loudness: 0x8000 = unset
fixed[422:602] = bytes(180) # reserved
coding = md.get("coding_history", "").encode("utf-8", errors="replace")
return bytes(fixed) + coding
def patch_bwf_metadata(path, metadata=None):
"""Chen bext + LIST/INFO vao WAV tai path (rewrite in-place, giu nguyen PCM bits).
metadata keys: title, artist, isrc, comment, description, originator,
originator_reference, date (YYYY-MM-DD), time (HH:MM:SS), coding_history.
Bo qua neu file khong phai RIFF/WAVE hop le hoac metadata rong.
"""
if not metadata:
return
md = {k: (v or "").strip() for k, v in metadata.items() if v}
if not md:
return
with open(path, "rb") as f:
data = f.read()
if len(data) < 12 or data[:4] != b"RIFF" or data[8:12] != b"WAVE":
return
off = 12
pre = b""
data_chunk = None
while off + 8 <= len(data):
cid = data[off:off + 4]
size = struct.unpack("<I", data[off + 4:off + 8])[0]
body = data[off + 8:off + 8 + size]
if cid == b"data":
data_chunk = (cid, size, body)
break
pre += _chunk(cid, body)
off += 8 + size + (size % 2)
if data_chunk is None:
return
now = datetime.datetime.now()
info_payload = b"INFO" + b"".join([
_info_sub(b"INAM", md.get("title", "")),
_info_sub(b"IART", md.get("artist", "")),
_info_sub(b"ISRC", md.get("isrc", "")),
_info_sub(b"ICMT", md.get("comment", "")),
])
extra = _chunk(b"bext", _bext(md, now)) + _chunk(b"LIST", info_payload)
new_data = data[:12] + pre + extra + _chunk(data_chunk[0], data_chunk[2])
new_data = new_data[:4] + struct.pack("<I", len(new_data) - 8) + new_data[8:]
with open(path, "wb") as f:
f.write(new_data)