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