diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 144734c..638f8cd 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -616,6 +616,8 @@ class RenderRequest(BaseModel): normalize: Optional[bool] = False normalize_target: Optional[str] = "-1dbtp" mono_check: Optional[bool] = False + # Gap 13: BWF/INFO metadata (title/artist/isrc/comment/description/...) + metadata: Optional[dict] = None class FxGuiRequest(BaseModel): @@ -1212,6 +1214,7 @@ def render_project( dither=bool(req.dither), normalize=bool(req.normalize), normalize_target=req.normalize_target or "-1dbtp", + metadata=req.metadata or None, ) # EBU R128 loudness + true peak (D1) — trả kèm response để mastering # panel hiển thị LUFS/dBTP ngay sau export mà không cần đọc lại file. diff --git a/app/api/v1/projects.py b/app/api/v1/projects.py index c5c8290..28c33c4 100644 --- a/app/api/v1/projects.py +++ b/app/api/v1/projects.py @@ -546,6 +546,7 @@ async def cleanup_all_backups(req: CleanupBackupsRequest, current_user: dict = D class RenderProjectRequest(BaseModel): sample_rate: Optional[int] = 44100 + metadata: Optional[dict] = None # Gap 13: BWF/INFO metadata (title/artist/isrc/...) @router.post("/cloud/{project_id}/render") async def render_project_endpoint(project_id: str, req: RenderProjectRequest, current_user: dict = Depends(get_current_user)): @@ -568,7 +569,8 @@ async def render_project_endpoint(project_id: str, req: RenderProjectRequest, cu project_id=project_id, project_name=row["name"], project_json_str=row["data_json"], - sample_rate=req.sample_rate or 44100 + sample_rate=req.sample_rate or 44100, + metadata=req.metadata or None ) return { diff --git a/app/core/render_engine.py b/app/core/render_engine.py index a4eb9c1..294ac53 100644 --- a/app/core/render_engine.py +++ b/app/core/render_engine.py @@ -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 diff --git a/app/core/wav_bwf.py b/app/core/wav_bwf.py new file mode 100644 index 0000000..1107133 --- /dev/null +++ b/app/core/wav_bwf.py @@ -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(" )} +
+
+ + setExportSettings(p => ({ ...p, metaTitle: e.target.value }))} + placeholder="Tên bài" className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none" /> +
+
+ + setExportSettings(p => ({ ...p, metaArtist: e.target.value }))} + placeholder="Nghệ sĩ" className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none" /> +
+
+ + setExportSettings(p => ({ ...p, metaIsrc: e.target.value.toUpperCase() }))} + placeholder="US-ABC-26-00001" className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none" /> +
+