Files
JustOneCacophony/src/oamd_bits.py
T

593 lines
24 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.
"""OAMD 位载荷 → 16 个对象槽的 q1/q2/q3 增量状态。
依据 ETSI TS 103 420 V1.2.1 clause 5。槽 0 为 bed/LFE,槽 1..15 为输出
ch1..15;bed/ISF/未激活对象没有位置字段,保持上一帧位置。element 目录按声明长度
驱动,未知 element 按边界跳过;个别编码器的 ``oa_element_size`` 比实际内容短时以
结构解析为准,差异记入 ``diagnostics``,不算变体错误。
"""
import numpy as np
from variant_error import UnsupportedVariantError, bytes_descriptor
N_Q12 = 62
N_Q3 = 15
SAMPLE_OFFSET_INDEX = (8, 16, 18, 24)
RAMP_DURATIONS = (0, 512, 1536)
RAMP_DURATION_INDEX = (
32, 64, 128, 256, 320, 480, 1000, 1001,
1024, 1600, 1601, 1602, 1920, 2000, 2002, 2048,
)
OBJECT_ELEMENT_ID = 1
# 5.6.0.11 Table 11b:ISF 类型 → 对象数。
ISF_OBJECT_COUNTS = (4, 8, 10, 14, 15, 30)
# 5.6.1.1.4 Table 12:10 bit 标准 bed 掩码按位对应的声道(LSB = RC_L/RC_R)。
BED_CHANNEL_LABELS = (
"RC_L/RC_R", "RC_C", "RC_LFE", "RC_LS/RC_RS", "RC_LB/RC_RB",
"RC_TFL/RC_TFR", "RC_TSL/RC_TSR", "RC_TBL/RC_TBR", "RC_LW/RC_RW", "RC_LFE2",
)
# 5.6.1.1.5 Table 13:17 bit 非标准 bed 掩码按位对应的声道标签。
NONSTD_BED_LABELS = (
"RC_LFE2", "RC_RW", "RC_LW", "RC_TBR", "RC_TBL", "RC_TSR", "RC_TSL",
"RC_TFR", "RC_TFL", "RC_RB", "RC_LB", "RC_RS", "RC_LS", "RC_LFE", "RC_C",
"RC_R", "RC_L",
)
MAX_SLOTS = 16
def q_of(k, n):
q = int(np.floor(32768.0 * k / n + 0.5))
return min(32767, q)
def _payload_bits(bits_one):
if isinstance(bits_one, (bytes, bytearray, memoryview)):
raw_payload = bytes(bits_one)
bits = np.unpackbits(
np.frombuffer(raw_payload, dtype=np.uint8), bitorder="big")
else:
src = np.asarray(bits_one)
if src.ndim != 1:
raw = np.asarray(bits_one, dtype=np.uint8).tobytes()
raise UnsupportedVariantError(
"oamd", "payload_shape",
"OAMD 载荷必须是一维 byte 或 bit 序列",
details={"shape": list(src.shape), "payload": bytes_descriptor(raw)})
is_bit_vector = bool(src.size) and bool(np.all((src == 0) | (src == 1)))
if is_bit_vector:
if src.size % 8:
raw = np.packbits(src.astype(np.uint8), bitorder="big").tobytes()
raise UnsupportedVariantError(
"oamd", "payload_bit_alignment",
"OAMD bit 载荷没有按整字节对齐",
details={
"payload_bits": int(src.size),
"payload": bytes_descriptor(raw),
})
bits = src.astype(np.uint8, copy=False)
raw_payload = np.packbits(bits, bitorder="big").tobytes()
else:
try:
values = src.astype(np.int64, copy=False)
except (TypeError, ValueError, OverflowError) as exc:
raise UnsupportedVariantError(
"oamd", "payload_type",
"OAMD 载荷不能转换为 byte 序列",
details={"dtype": str(src.dtype), "parser_error": str(exc)}) from exc
if np.any(values < 0) or np.any(values > 255):
raise UnsupportedVariantError(
"oamd", "payload_byte_range",
"OAMD byte 载荷包含 0..255 之外的值",
details={"dtype": str(src.dtype), "payload_values": int(src.size)})
raw_payload = values.astype(np.uint8).tobytes()
bits = np.unpackbits(
np.frombuffer(raw_payload, dtype=np.uint8), bitorder="big")
return bits, raw_payload
class _BitReader:
def __init__(self, bits, position=0, limit=None):
self.bits = bits
self.position = int(position)
self.limit = len(bits) if limit is None else int(limit)
def read(self, count):
end = self.position + count
if end > self.limit:
raise ValueError(f"OAMD 位流越界: bit={self.position}, need={count}")
value = 0
for bit in self.bits[self.position:end]:
value = (value << 1) | int(bit)
self.position = end
return value
def skip(self, count):
self.read(count)
def _variable_bits_max(reader, width, max_groups):
"""5.5.1 ``variable_bits_max(n, max_num_groups)``。"""
value = reader.read(width)
more = reader.read(1)
num_group = 1
if max_groups > num_group:
if more:
value = (value + 1) << width
while more:
value += reader.read(width)
more = reader.read(1)
if num_group >= max_groups:
break
if more:
value = (value + 1) << width
num_group += 1
return value
def _element_details(elements):
return [{
"ordinal": element["ordinal"],
"element_id": element["element_id"],
"size_bytes": element["size_bytes"],
"header_start_bit": element["header_start_bit"],
"body_start_bit": element["body_start_bit"],
"body_end_bit": element["body_end_bit"],
"parsed_end_bit": element.get("parsed_end_bit"),
"discard_unknown": element["discard_unknown"],
"alternate_data_id": element["alternate_data_id"],
} for element in elements]
def _syntax_error(variant, message, raw_payload, exc=None, **details):
payload = {"payload": bytes_descriptor(raw_payload)}
if exc is not None:
payload["parser_error"] = str(exc)
payload.update(details)
return UnsupportedVariantError("oamd", variant, message, details=payload)
def _parse_program_assignment(reader, raw_payload):
"""5.5.3 ``program_assignment()``:bed / ISF / dynamic 对象数量。"""
program = {
"dynamic_object_only": bool(reader.read(1)),
"lfe_present": False,
"content_description": None,
"bed_assignments": [],
"num_bed_objects": 0,
"isf_idx": None,
"num_isf_objects": 0,
"num_dynamic_objects": None,
}
if program["dynamic_object_only"]:
program["lfe_present"] = bool(reader.read(1))
# 5.6.4.8:对象顺序为 bed → ISF → dynamic,LFE 属于 bed,排在最前。
program["num_bed_objects"] = 1 if program["lfe_present"] else 0
return program
mask = reader.read(4)
program["content_description"] = {
"reserved": bool(mask & 0x8),
"dynamic": bool(mask & 0x4),
"isf": bool(mask & 0x2),
"bed": bool(mask & 0x1),
}
if mask & 0x1:
program["b_bed_chan_distribute"] = bool(reader.read(1))
num_instances = (reader.read(3) + 2) if reader.read(1) else 1
for instance in range(num_instances):
if reader.read(1): # b_lfe_only
program["bed_assignments"].append({
"instance": instance, "lfe_only": True, "channels": ["RC_LFE"],
})
continue
if reader.read(1): # b_standard_chan_assign
bed_mask = reader.read(10)
channels = []
for bit in range(10):
if (bed_mask >> bit) & 1:
channels.extend(BED_CHANNEL_LABELS[bit].split("/"))
program["bed_assignments"].append({
"instance": instance, "lfe_only": False, "standard": True,
"mask": bed_mask, "channels": channels,
})
else:
bed_mask = reader.read(17)
channels = [NONSTD_BED_LABELS[bit]
for bit in range(17) if (bed_mask >> bit) & 1]
program["bed_assignments"].append({
"instance": instance, "lfe_only": False, "standard": False,
"mask": bed_mask, "channels": channels,
})
program["num_bed_objects"] = sum(
len(assignment["channels"]) for assignment in program["bed_assignments"])
if mask & 0x2:
isf_idx = reader.read(3)
program["isf_idx"] = isf_idx
program["num_isf_objects"] = (ISF_OBJECT_COUNTS[isf_idx]
if isf_idx < len(ISF_OBJECT_COUNTS) else 0)
if mask & 0x4:
count = reader.read(5)
if count == 0x1F:
count += reader.read(7)
program["num_dynamic_objects"] = count + 1
if mask & 0x8:
# 5.6.4.1:reserved_data_size = reserved_data_size_bits + 1(字节)。
reader.skip((reader.read(4) + 1) * 8)
return program
def _parse_object_info_block(reader, object_index, in_bed_or_isf, raw_payload):
"""5.5.9 ``object_info_block(0)``:一个对象的属性更新。"""
info = {
"object": object_index,
"in_bed_or_isf": bool(in_bed_or_isf),
"not_active": bool(reader.read(1)),
}
# blk == 0 且对象激活时 basic info 恒为 full update(status 0b01)。
basic_status = 0 if info["not_active"] else 1
info["basic_status"] = basic_status
if basic_status in (1, 3):
if basic_status == 1:
# 5.5.10:object_basic_info[] = {true, true};
# 5.6.4.12:array[1] = object_gain_idx,array[0] = b_default_object_priority。
gain_present = priority_present = True
else:
flags = reader.read(2)
gain_present = bool(flags & 1)
priority_present = bool(flags & 2)
if gain_present:
gain_idx = reader.read(2)
info["gain_idx"] = gain_idx
if gain_idx == 2:
info["gain_bits"] = reader.read(6)
if priority_present:
default_priority = bool(reader.read(1))
info["default_priority"] = default_priority
if not default_priority:
info["priority_bits"] = reader.read(5)
render_status = 0 if (info["not_active"] or info["in_bed_or_isf"]) else 1
info["render_status"] = render_status
if render_status in (1, 3):
if render_status == 1:
# 5.5.11:obj_render_info[] = {true, true, true, true}。
position_present = zone_present = size_present = screen_present = True
else:
flags = reader.read(4)
position_present = bool(flags & 0x1)
zone_present = bool(flags & 0x2)
size_present = bool(flags & 0x4)
screen_present = bool(flags & 0x8)
if position_present:
# blk == 0 时 b_differential_position_specified 恒为 FALSE。
x = reader.read(6)
y = reader.read(6)
z_sign = reader.read(1)
z = reader.read(4)
info["position"] = (x, y, z if z_sign else -z)
if reader.read(1): # b_object_distance_specified
if not reader.read(1): # b_object_at_infinity
reader.read(4) # distance_factor_idx
if zone_present:
info["zone_constraints_idx"] = reader.read(3)
info["enable_elevation"] = bool(reader.read(1))
if size_present:
size_idx = reader.read(2)
info["object_size_idx"] = size_idx
if size_idx == 1:
reader.read(5)
elif size_idx == 2:
reader.read(15)
if screen_present:
if reader.read(1): # b_object_use_screen_ref
reader.read(3) # screen_factor_bits
reader.read(2) # depth_factor_idx
info["snap"] = bool(reader.read(1))
if reader.read(1): # b_additional_table_data_exists
info["additional_table_bytes"] = reader.read(4) + 1
reader.skip(info["additional_table_bytes"] * 8)
return info
def _parse_object_element(reader, element, raw_payload):
"""5.5.5 ``object_element()``:时间信息 + 每个对象一条 object_info_block。"""
object_count = element["object_count"]
try:
sample_offset_code = reader.read(2)
if sample_offset_code == 0:
sample_offset = 0
elif sample_offset_code == 1:
sample_offset = SAMPLE_OFFSET_INDEX[reader.read(2)]
elif sample_offset_code == 2:
sample_offset = reader.read(5)
else:
raise UnsupportedVariantError(
"oamd", "md_sample_offset_mode",
"OAMD 使用了当前未覆盖的 MD sample-offset 模式",
details={
"sample_offset_code": sample_offset_code,
"payload": bytes_descriptor(raw_payload),
})
block_count = reader.read(3) + 1
blocks = []
for _block in range(block_count):
block_offset_factor = reader.read(6)
ramp_code = reader.read(2)
if ramp_code == 3:
if reader.read(1):
ramp_duration = RAMP_DURATION_INDEX[reader.read(4)]
else:
ramp_duration = reader.read(11)
else:
ramp_duration = RAMP_DURATIONS[ramp_code]
blocks.append({
"block_offset_samples": sample_offset + block_offset_factor * 32,
"ramp_duration_samples": ramp_duration,
})
reserved_data_not_present = bool(reader.read(1))
if not reserved_data_not_present:
reader.read(5)
objects = [
_parse_object_info_block(
reader, index, index < element["bed_isf_objects"], raw_payload)
for index in range(object_count)
]
except UnsupportedVariantError:
raise
except ValueError as exc:
raise _syntax_error(
"object_element_syntax",
"OAMD object element 字段越界或不完整",
raw_payload, exc,
element=_element_details([element])[0],
object_count=object_count,
) from exc
return {
"sample_offset": sample_offset,
"blocks": blocks,
"reserved_data_not_present": reserved_data_not_present,
"objects": objects,
}
def _parse_elements(reader, alternate_present, object_count, program, raw_payload):
"""5.5.4 ``oa_element_md()`` 目录;对象 element 按结构解析,其余按声明长度跳过。"""
try:
element_count = reader.read(4)
if element_count == 0xF:
element_count += reader.read(5)
except ValueError as exc:
raise _syntax_error(
"element_count_truncated", "OAMD element 数量字段不完整",
raw_payload, exc) from exc
if element_count == 0:
raise UnsupportedVariantError(
"oamd", "missing_object_element",
"OAMD 没有声明任何 element",
details={"payload": bytes_descriptor(raw_payload)})
bed_isf_objects = program["num_bed_objects"] + program["num_isf_objects"]
elements = []
object_element = None
for ordinal in range(element_count):
header_start = reader.position
try:
element_id = reader.read(4)
size_bytes = _variable_bits_max(reader, 4, 4) + 1
except ValueError as exc:
raise _syntax_error(
"element_header_truncated", "OAMD element header 不完整",
raw_payload, exc,
element_ordinal=ordinal, header_start_bit=header_start) from exc
region_start = reader.position
region_end = region_start + size_bytes * 8
if region_end > reader.limit:
raise UnsupportedVariantError(
"oamd", "element_bounds",
"OAMD element 声明长度超过 payload 边界",
details={
"element_ordinal": ordinal,
"element_id": element_id,
"size_bytes": size_bytes,
"body_start_bit": region_start,
"body_end_bit": region_end,
"payload_bits": reader.limit,
"payload": bytes_descriptor(raw_payload),
})
try:
alternate_data_id = (reader.read(4)
if alternate_present else None)
discard_unknown = bool(reader.read(1))
except ValueError as exc:
raise _syntax_error(
"element_control_bounds", "OAMD element 太短,无法容纳控制字段",
raw_payload, exc,
element_ordinal=ordinal, element_id=element_id,
size_bytes=size_bytes) from exc
element = {
"ordinal": ordinal,
"element_id": element_id,
"size_bytes": size_bytes,
"header_start_bit": header_start,
"body_start_bit": region_start,
"body_end_bit": region_end,
"data_start_bit": reader.position,
"alternate_data_id": alternate_data_id,
"discard_unknown": discard_unknown,
"object_count": object_count,
"bed_isf_objects": bed_isf_objects,
}
if element_id == OBJECT_ELEMENT_ID:
if object_element is not None:
raise UnsupportedVariantError(
"oamd", "multiple_object_elements",
"OAMD 含多个 object element",
details={
"elements": _element_details(elements + [element]),
"payload": bytes_descriptor(raw_payload),
})
object_element = _parse_object_element(
reader, element, raw_payload)
element["parsed_end_bit"] = reader.position
element["object_element"] = object_element
# 个别编码器声明的 oa_element_size 比实际内容短;以结构解析结果为准。
reader.position = max(region_end, reader.position)
else:
# trim / extended / 未知 element:按声明长度整体跳过(5.5.4)。
element["parsed_end_bit"] = region_end
reader.position = region_end
elements.append(element)
if object_element is None:
raise UnsupportedVariantError(
"oamd", "missing_object_element",
"OAMD 缺少 object element",
details={
"elements": _element_details(elements),
"payload": bytes_descriptor(raw_payload),
})
padding = reader.bits[reader.position:]
if np.any(padding):
raise UnsupportedVariantError(
"oamd", "nonzero_padding",
"OAMD payload 尾部 padding 含非零位",
details={
"elements": _element_details(elements),
"padding_start_bit": reader.position,
"padding_bits": "".join(str(int(bit)) for bit in padding[:64]),
"payload": bytes_descriptor(raw_payload),
})
return object_element, elements
def frame_update(bits_one):
"""单帧 OAMD → 位置字段增量及其 sample offset/ramp duration。"""
bits, raw_payload = _payload_bits(bits_one)
try:
version = bits[0] << 1 | bits[1]
except IndexError:
raise UnsupportedVariantError(
"oamd", "header_truncated",
"OAMD payload 不足以容纳 header",
details={"payload_bits": len(bits),
"payload": bytes_descriptor(raw_payload)}) from None
reader = _BitReader(bits, 2)
try:
if version == 3:
version += reader.read(3)
object_count_bits = reader.read(5)
if object_count_bits == 0x1F:
object_count_bits += reader.read(7)
object_count = object_count_bits + 1
program = _parse_program_assignment(reader, raw_payload)
alternate_present = bool(reader.read(1))
object_element, elements = _parse_elements(
reader, alternate_present, object_count, program, raw_payload)
except UnsupportedVariantError:
raise
except ValueError as exc:
raise _syntax_error(
"header_truncated", "OAMD header 字段越界或不完整",
raw_payload, exc, payload_bits=len(bits)) from exc
if version != 0:
raise UnsupportedVariantError(
"oamd", "oamd_version",
"OAMD 使用了当前未覆盖的 syntax version",
details={
"version": version,
"payload": bytes_descriptor(raw_payload),
})
if object_count > MAX_SLOTS:
raise UnsupportedVariantError(
"oamd", "object_count",
"OAMD 对象数超过当前 16 槽对象模型",
details={
"object_count": object_count,
"max_slots": MAX_SLOTS,
"payload": bytes_descriptor(raw_payload),
})
blocks = object_element["blocks"]
if len(blocks) != 1:
raise UnsupportedVariantError(
"oamd", "multiple_position_blocks",
"OAMD 一帧含多个对象位置更新块,单次 frame_update 无法表达",
details={
"block_count": len(blocks),
"blocks": blocks,
"payload": bytes_descriptor(raw_payload),
"repair_hint": "按 ObjectInfoBlock 顺序逐块解析坐标,再生成分段 ADM ramp",
})
# 对外契约:始终给出 16 个槽;本帧没有覆盖到的槽保持上一帧位置。
out = {(slot, field): None for slot in range(MAX_SLOTS)
for field in ("q1", "q2", "q3")}
for info in object_element["objects"]:
slot = info["object"]
position = info.get("position")
if position is None:
# bed/ISF 对象与未激活对象没有位置字段:保持上一帧位置。
out[(slot, "q1")] = None
out[(slot, "q2")] = None
out[(slot, "q3")] = None
continue
x, y, z = position
out[(slot, "q1")] = q_of(x, N_Q12) if 0 <= x <= N_Q12 else None
out[(slot, "q2")] = q_of(y, N_Q12) if 0 <= y <= N_Q12 else None
# 5.6.1.1.10/5.6.1.1.11:pos3D_Z 带符号;当前 16 槽模型只表达非负高度。
out[(slot, "q3")] = q_of(z, N_Q3) if 0 <= z <= N_Q3 else (
0 if z < 0 else None)
size_mismatch = [
{
"element_ordinal": element["ordinal"],
"element_id": element["element_id"],
"declared_size_bytes": element["size_bytes"],
"declared_end_bit": element["body_end_bit"],
"parsed_end_bit": element["parsed_end_bit"],
}
for element in elements
if element["parsed_end_bit"] > element["body_end_bit"]
]
return {
"values": out,
"block_offset_samples": blocks[0]["block_offset_samples"],
"ramp_duration_samples": blocks[0]["ramp_duration_samples"],
"object_count": object_count,
"program": {
"dynamic_object_only": program["dynamic_object_only"],
"lfe_present": program["lfe_present"],
"content_description": program["content_description"],
"num_bed_objects": program["num_bed_objects"],
"num_isf_objects": program["num_isf_objects"],
"num_dynamic_objects": program["num_dynamic_objects"],
"bed_channels": [list(assignment["channels"])
for assignment in program["bed_assignments"]],
},
"elements": _element_details(elements),
"objects": object_element["objects"],
"size_mismatch": size_mismatch,
}
def frame_update_values(bits_one):
"""兼容接口:只返回 ``{(slot, field): q|None}``。"""
return frame_update(bits_one)["values"]
class JocFieldState:
"""未更新/非法窗保持旧值;slot0 q1/q2 的 DLL 初值为中心 16384。"""
def __init__(self):
self.q = {(obj, field): 0 for obj in range(16)
for field in ("q1", "q2", "q3")}
self.q[(0, "q1")] = 16384
self.q[(0, "q2")] = 16384
def apply(self, updates):
for key, value in updates.items():
if value is not None:
self.q[key] = value
return self
def snapshot(self):
return dict(self.q)