Files
JustOneCacophony/src/oamd_tracks.py
T
TheM14 428772eb87
Native builds / linux-x64 (push) Failing after 21s
Native builds / macos-arm64 (push) Has been cancelled
Native builds / macos-x64 (push) Has been cancelled
Native builds / windows-x64 (push) Has been cancelled
Native builds / Publish GitHub Release (push) Has been cancelled
Fix OAMD ramp timing and band-0 DC filter gating
2026-09-27 00:55:13 +08:00

242 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Evolution id11/OAMD 帧序列 → ADM 15 对象关键帧。"""
import math
from adm_atmos import q_to_adm_xyz
from oamd_bits import JocFieldState, frame_update
from variant_error import UnsupportedVariantError
# 元数据更新时刻按渲染器处理块对齐,与 Dolby 的 processing_block_size 及
# speaker_renderer 的 block_size 落在同一网格。
METADATA_BLOCK_SAMPLES = 32
def align_metadata_sample(sample, block_samples=METADATA_BLOCK_SAMPLES):
"""把更新时刻量化到处理块边界。"""
block = int(block_samples)
if block <= 0:
raise ValueError("block_samples must be positive")
return block * ((int(sample) + block // 2 - 1) // block)
def _lerp_xyz(start, target, amount):
return tuple(a + (b - a) * amount for a, b in zip(start, target))
def _append_point(points, sample, xyz, interpolation_samples):
item = (int(sample), *map(float, xyz), int(interpolation_samples))
if points and item[0] == points[-1][0]:
points[-1] = item
elif not points or item[0] > points[-1][0]:
points.append(item)
def _expand_events_dense64(events, total_samples, rate, update_block_samples,
object_delay_samples, object_index):
if not events:
return [(0.0, 0.0, 0.0, 0.0, total_samples / float(rate), 0.0)]
block = int(update_block_samples)
# 初始位置从成品 sample 0 起有效;合成延迟只作用于后续位置变化。
current = events[0][1]
points = []
_append_point(points, 0, current, 0)
for event_index, (coded_start, target, ramp_samples) in enumerate(events[1:], 1):
start = align_metadata_sample(coded_start + object_delay_samples, block)
if start >= total_samples:
break
if start < points[-1][0]:
raise UnsupportedVariantError(
"oamd", "non_monotonic_position_updates",
"对象位置更新时间倒退,无法生成连续 ADM 轨迹",
details={"object": object_index, "sample": start,
"previous_sample": points[-1][0]})
# 在运动起点保留上一位置,避免下游把长时间静止段直接连到首个中间点。
if start > points[-1][0]:
_append_point(points, start, current, 0)
ramp = max(0, int(ramp_samples))
if ramp == 0:
_append_point(points, start, target, 0)
current = target
continue
end = start + math.ceil(ramp / block) * block
if event_index + 1 < len(events):
next_start = align_metadata_sample(
events[event_index + 1][0] + object_delay_samples, block)
if next_start < end:
raise UnsupportedVariantError(
"oamd", "overlapping_position_ramps",
"同一对象的新位置更新在上一 ramp 完成前到达",
details={
"object": object_index,
"ramp_start_sample": start,
"ramp_end_sample": end,
"next_update_sample": next_start,
"repair_hint": "按 32-sample 状态机截断旧 ramp,再从当前插值位置启动新 ramp",
})
# 从对齐后的更新点起按处理块推进,整条 ramp 覆盖 ramp_samples 个样本;
# 几何式逼近望远镜化简为精确线性,每个中间点都落在真实 ramp 上。
future = ramp
elapsed = 0
position = current
while future > 0:
amount = min(block / float(future), 1.0)
position = _lerp_xyz(position, target, amount)
elapsed += block
sample = start + elapsed
if sample >= total_samples:
break
_append_point(points, sample, position, block)
future -= block
current = target
blocks = []
for point_index, (sample, x, y, z, interpolation_samples) in enumerate(points):
end = points[point_index + 1][0] if point_index + 1 < len(points) else total_samples
duration_samples = max(0, end - sample)
if duration_samples == 0:
continue
interpolation_samples = min(interpolation_samples, duration_samples)
blocks.append((sample / float(rate), x, y, z,
duration_samples / float(rate),
interpolation_samples / float(rate)))
return blocks
def _compact_events(events, total_samples, rate, update_block_samples,
object_delay_samples, object_index):
"""Represent each linear OAMD ramp with one ADM interpolation block.
The update instant is quantized to the processing block boundary, the motion
starts there immediately, and ``interpolationLength`` spans the full
``ramp_duration``. The interpreted motion therefore covers
``[align(start), align(start) + ramp_duration]``.
"""
if not events:
return [(0.0, 0.0, 0.0, 0.0, total_samples / float(rate), 0.0)]
points = []
current = events[0][1]
_append_point(points, 0, current, 0)
block = int(update_block_samples)
for event_index, (coded_start, target, ramp_samples) in enumerate(events[1:], 1):
event_start = align_metadata_sample(coded_start + object_delay_samples, block)
if event_start >= total_samples:
break
ramp = max(0, int(ramp_samples))
block_start = event_start
if block_start >= total_samples:
break
ramp_end = block_start + ramp
if block_start < points[-1][0]:
raise UnsupportedVariantError(
"oamd", "non_monotonic_compact_position_updates",
"紧凑对象位置更新时间倒退",
details={"object": object_index, "sample": block_start,
"previous_sample": points[-1][0]})
if event_index + 1 < len(events):
next_coded_start, _, next_ramp_samples = events[event_index + 1]
next_event_start = align_metadata_sample(
next_coded_start + object_delay_samples, block)
next_block_start = next_event_start
if next_block_start < ramp_end:
raise UnsupportedVariantError(
"oamd", "overlapping_compact_position_ramps",
"同一对象的新位置更新在上一紧凑 ramp 完成前到达",
details={
"object": object_index,
"ramp_start_sample": block_start,
"ramp_end_sample": ramp_end,
"next_update_sample": next_block_start,
"repair_hint": "对此变体使用 --trajectory-mode dense64 并检查 OAMD 调度",
})
block_target = target
block_interpolation = ramp
available = total_samples - block_start
if ramp > available:
block_target = _lerp_xyz(current, target, available / float(ramp))
block_interpolation = available
_append_point(points, block_start, block_target, block_interpolation)
current = target
blocks = []
for point_index, (sample, x, y, z, interpolation_samples) in enumerate(points):
end = points[point_index + 1][0] if point_index + 1 < len(points) else total_samples
duration_samples = max(0, end - sample)
if duration_samples == 0:
continue
interpolation_samples = min(interpolation_samples, duration_samples)
blocks.append((sample / float(rate), x, y, z,
duration_samples / float(rate),
interpolation_samples / float(rate)))
return blocks
def _expand_events(events, total_samples, rate, update_block_samples,
object_delay_samples, object_index, trajectory_mode):
if trajectory_mode == "compact":
return _compact_events(events, total_samples, rate, update_block_samples,
object_delay_samples, object_index)
if trajectory_mode == "dense64":
return _expand_events_dense64(events, total_samples, rate, update_block_samples,
object_delay_samples, object_index)
raise ValueError(f"未知 trajectory_mode: {trajectory_mode}")
def build_adm_tracks(index, frames=None, rate=48000, frame_samples=1536,
update_block_samples=METADATA_BLOCK_SAMPLES,
object_delay_samples=1473, trajectory_mode="compact"):
"""从统一 metadata index 构造 15 条 ADM 轨迹。
返回 ``[(name, [(rtime,x,y,z,duration,interpolation), ...]), ...]``。
OAMD 的内外层 sample offset、block offset 和 ramp 均保留;更新时刻量化到
``update_block_samples`` 的块边界,ramp 覆盖完整的 ramp_duration。
``trajectory_mode="compact"`` 用一个长 ADM interpolation block 表示每条
线性 ramp;``dense64`` 保留逐块展开作为兼容回退。
``object_delay_samples`` 将位置更新与对象 PCM 的 decoder 输出时刻对齐。
slot1..15 与对象 PCM ch1..15 一一对应。
"""
frames = index.rows if frames is None else frames
state = JocFieldState()
events = [[] for _ in range(15)]
previous = [None] * 15
for seq, row in enumerate(frames):
subs = index.subpayloads(row)
timing = None
if 11 in subs:
timing = frame_update(subs[11])
state.apply(timing["values"])
event_sample = seq * frame_samples
ramp_samples = 0
if timing is not None:
outer_offset = (index.subpayload_sample_offset(row, 11)
if hasattr(index, "subpayload_sample_offset") else 0)
event_sample += outer_offset + timing["block_offset_samples"]
ramp_samples = timing["ramp_duration_samples"]
q = state.q
for obj in range(1, 16):
xyz = q_to_adm_xyz(q[(obj, "q1")], q[(obj, "q2")], q[(obj, "q3")])
if previous[obj - 1] != xyz:
events[obj - 1].append((event_sample, xyz, ramp_samples))
previous[obj - 1] = xyz
total_samples = len(frames) * frame_samples
return [
(f"JOC_Object_{obj}",
_expand_events(events[obj - 1], total_samples, rate,
update_block_samples, object_delay_samples, obj,
trajectory_mode))
for obj in range(1, 16)
]