89 lines
3.2 KiB
Python
89 lines
3.2 KiB
Python
"""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)
|