Initial public release of JustOneCacophony

This commit is contained in:
2026-09-01 16:31:38 +08:00
commit aa3a519f79
38 changed files with 9073 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
"""把 LFE、15 路对象 PCM 和对象轨迹组装为 ADM BWF。
固定输出契约:
EAC3JOC 重放输出 = 16ch(ch0 = LFE + ch1-15 = 15 对象);
最终 ADM BWF = 7.1.2 bed(L R C Ls Rs Lb Rb + LFE + Ltf Rtf = 10ch)
—— 除 LFE 外全部静音;
15 对象 = ch1-15 直接填充对象轨;轨迹 = OAMD(q1/q2/q3 → xyz)。
"""
import os
import numpy as np
import adm_atmos
def assemble_from_raw(raw16_path, out_path, scale=1.0, kf_tracks=None,
duration_sec=None, rate=48000):
"""16ch f32 交织 raw → 25ch ADM BWF(空 7.1.2 bed + LFE + 15 对象)。
raw16: (n, 16) 交织(ch0 = LFE,ch1-15 = 对象)。
scale: 1.0 = 默认 0 dB,不附加输出缩放。该参数与 joc_clipgain 无关;
主命令行已在渲染阶段应用用户增益,因此这里传 1.0。
kf_tracks: 可选轨迹关键帧(OAMD 输出,格式 [(obj_id, [(t, x, y, z), ...]), ...]);
缺省 = 静止参考位置(adm_atmos 默认)。
"""
raw = np.memmap(raw16_path, dtype=np.float32, mode="r")
n = len(raw) // 16
raw = raw[:n * 16].reshape(-1, 16)
if duration_sec is None:
duration_sec = n / rate
# 惰性视图:adm_atmos 按块读取,避免全片 25ch 在内存中展开。
class BedView:
shape = (n, 10)
def __getitem__(self, key):
src = np.asarray(raw[key], dtype=np.float32)
one = src.ndim == 1
if one:
src = src[None, :]
out = np.zeros((len(src), 10), dtype=np.float32)
out[:, 3] = np.multiply(src[:, 0], np.float32(scale), dtype=np.float32)
return out[0] if one else out
class ObjView:
shape = (n, 16)
def __getitem__(self, key):
return np.multiply(np.asarray(raw[key], dtype=np.float32),
np.float32(scale), dtype=np.float32)
if kf_tracks is None:
kf_tracks = []
for oi in range(15):
# 静止参考位置(q1=q2=q3=0 → 原点;实际坐标按 OAMD 输出填入)
kf_tracks.append(("JOC_Object_%d" % (oi + 1),
[(0.0, 0.0, 0.0, 0.0, max(duration_sec, 1e-6))]))
adm_atmos.build_master(out_path, BedView(), ObjView(), kf_tracks,
duration_sec, rate=rate)
# 及时释放 Windows 文件句柄,允许 TemporaryDirectory 删除中间 raw。
raw._mmap.close()
return out_path
class StreamingMaster:
"""Incrementally write renderer frames into the final 25-channel ADM BWF.
This removes the default 16-channel float32 intermediate file. The mapping
remains identical to :func:`assemble_from_raw`: bed channel 3 receives LFE,
bed channels 0..2/4..9 are silent, and output objects 1..15 map to ADM
channels 10..24.
"""
def __init__(self, out_path, duration_sec, rate=48000, block_samples=131072):
if block_samples < 1536:
raise ValueError("block_samples must be at least one E-AC-3 frame")
self.out_path = os.fspath(out_path)
self.duration_sec = float(duration_sec)
self.rate = int(rate)
self._sink = adm_atmos.Sink25(self.out_path, 25, self.rate)
self._buffer = np.empty((int(block_samples), 25), dtype=np.float32)
self._used = 0
self._finalized = False
def _flush(self):
if self._used:
self._sink.write_block(self._buffer[:self._used])
self._used = 0
def write_frame(self, pcm16):
pcm = np.asarray(pcm16, dtype=np.float32)
if pcm.shape != (16, 1536):
raise ValueError(f"renderer frame must be (16,1536), got {pcm.shape}")
source = 0
while source < 1536:
available = len(self._buffer) - self._used
count = min(available, 1536 - source)
target = self._buffer[self._used:self._used + count]
target.fill(0.0)
target[:, 3] = pcm[0, source:source + count]
target[:, 10:25] = pcm[1:16, source:source + count].T
self._used += count
source += count
if self._used == len(self._buffer):
self._flush()
def finalize(self, kf_tracks):
if self._finalized:
raise RuntimeError("StreamingMaster already finalized")
self._flush()
try:
from . import adm_serializer
except ImportError:
import adm_serializer
axml = adm_serializer.build_axml(kf_tracks, self.duration_sec)
chna = adm_atmos.build_chna()
dbmd = adm_atmos.build_dbmd(25)
trajectory_blocks = sum(len(track[1]) for track in kf_tracks)
self.metadata_info = {
"axml_bytes": len(axml),
"trajectory_blocks": trajectory_blocks,
"chna_bytes": len(chna),
"dbmd_bytes": len(dbmd),
}
self._sink.finalize(axml, chna, dbmd)
self._finalized = True
print(f"master25 -> {self.out_path} ({self.duration_sec:.2f}s, 25ch, "
f"axml={len(axml)}B, chna={len(chna)}B, dbmd={len(dbmd)}B)")
return self.out_path
def abort(self):
if self._finalized:
return
sink = getattr(self, "_sink", None)
fp = getattr(sink, "fp", None)
if fp is not None and not fp.closed:
fp.close()
def __del__(self):
try:
self.abort()
except Exception:
pass