146 lines
5.6 KiB
Python
146 lines
5.6 KiB
Python
"""把 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, joc_binaural_mode=4):
|
||
"""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,
|
||
joc_binaural_mode=joc_binaural_mode)
|
||
# 及时释放 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,
|
||
joc_binaural_mode=4):
|
||
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.joc_binaural_mode = joc_binaural_mode
|
||
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, joc_binaural_mode=self.joc_binaural_mode)
|
||
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
|