Files
SonicForgeStudio/app/core/wav_bwf.py
T

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)