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
+15
View File
@@ -0,0 +1,15 @@
__pycache__/
*.py[cod]
.venv/
venv/
build/
output/
metadata_cache/
*.metadata.json
*.report.json
*.variant-error.json
*.objects16.f32le
+167
View File
@@ -0,0 +1,167 @@
# JustOneCacophony — JOC
[中文版](README.md)
> JustOneCacophony is an experimental/test implementation of E-AC-3 JOC for studying JOC parsing, reconstruction, rendering, and the associated mathematics.
The project can extract and parse EMDF, ID14 JOC parameters, and ID11 OAMD metadata from common E-AC-3 JOC streams. It combines those data with the core 5.1 PCM decoded by FFmpeg, reconstructs LFE plus 15 object channels, and writes either ADM BWF or a WAV file for a selected speaker layout.
This is research code, not a complete, standards-compliant, or production-grade Dolby JOC decoder. It covers only the stream forms currently implemented. Unknown variants fail explicitly—because when the math goes wrong, all that may remain is the cacophony.
## Current features
- Scan common contiguous EMDF containers in E-AC-3 sync frames.
- Parse ID14 dense JOC parameters, Huffman data, differential matrices, and `joc_clipgain`.
- Parse ID11 OAMD position updates and build object trajectories.
- Reconstruct LFE plus 15 object channels through analysis QMF, parameter interpolation, the object matrix, and inverse QMF.
- Write a 25-channel ADM BWF: a 10-channel 7.1.2 bed (silent except for LFE) plus 15 objects.
- Render directly to `2.0`, `3.1`, `5.1`, `7.1`, `5.1.2`, `5.1.4`, `7.1.2`, `7.1.4`, `9.1.4`, or `9.1.6`.
- Write float32 or PCM24 WAV and require an explicit policy when PCM24 would clip.
- Use the NumPy backend or an optional C++20 core through `ctypes`; `auto` falls back to Python when the native library is unavailable.
- Read or write metadata sidecars and produce metadata, timing, and output reports.
## Processing flow
```text
M4A / E-AC-3
├─ FFmpeg extracts E-AC-3 and decodes the core 5.1 PCM
├─ EMDF → ID14 JOC parameters → object matrix
├─ core PCM → analysis QMF → parameter interpolation → inverse QMF
├─ ID11 OAMD → object positions and timing
└─ LFE + 15 objects
├─ 25ch ADM BWF
└─ speaker WAV for the selected layout
```
The Python and C++ backends follow the same documented mathematics. The native core handles the state-heavy DSP and speaker rendering; high-level bitstream parsing, ADM assembly, and CLI behavior remain in Python.
## Requirements
- Python 3.10+
- NumPy 1.24+
- A standalone FFmpeg executable; `ffmpeg-python` is not required. FFmpeg is discovered through `PATH` by default or selected with `--ffmpeg`
- Optional: CMake and a C++20 toolchain to build the native core
Install the Python dependency in a project-specific environment:
```powershell
python -m pip install -r requirements.txt
```
If FFmpeg is not on `PATH`:
```powershell
python main.py input.m4a --ffmpeg C:\path\to\ffmpeg.exe
```
## Usage
Write a 25-channel ADM BWF by default:
```powershell
python main.py input.m4a
```
Select a backend or output path:
```powershell
python main.py input.eac3 -o output.adm.wav --backend python
python main.py input.m4a --backend native --native-threads 2
python main.py input.m4a --native-library lib/eac3joc_core.dll
```
Write a speaker-layout WAV directly:
```powershell
python main.py input.m4a --speaker-layout 2.0 --speaker-format float32
python main.py input.m4a --speaker-layout 5.1 --speaker-format int24
python main.py input.m4a --speaker-layout 7.1.2 --speaker-output output.7.1.2.wav
```
When PCM24 may clip in a non-interactive environment, select a policy explicitly:
```powershell
python main.py input.m4a --speaker-layout 5.1 --speaker-format int24 --clip-action abort
python main.py input.m4a --speaker-layout 5.1 --speaker-format int24 --clip-action float32
python main.py input.m4a --speaker-layout 5.1 --speaker-format int24 --clip-action continue
```
Metadata and diagnostics:
```powershell
python main.py input.m4a --print-metadata summary
python main.py input.m4a --metadata-only --print-metadata frames
python main.py input.m4a --metadata-cache metadata_cache
python main.py input.m4a --metadata-dir metadata_cache
```
For all options:
```powershell
python main.py --help
```
Without `-o`, output still goes to `output/` at the repository root. The directory move intentionally preserves this behavior.
## Native core
The repository does not include native binaries by default. Download a prebuilt runtime for the current platform from a project Release, or build one locally, then place the runtime library under `lib/` at the repository root; create the directory if it is absent. To build it yourself, run CMake from the repository root:
```powershell
cmake -S native -B build/cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$PWD/lib"
cmake --build build/cmake --config Release
cmake --install build/cmake --config Release
```
The runtime lookup order is:
1. `--native-library`;
2. `EAC3JOC_NATIVE_LIBRARY`;
3. the standard platform library name under `lib/`.
See the [native-core notes](docs/native.en.md) for ABI, state, and precision details.
## Repository layout
```text
JustOneCacophony/
├─ main.py command-line entry point
├─ src/ Python implementation modules
├─ native/ C/C++ acceleration core, C ABI, and required table data
├─ data/ runtime table data for Python
├─ lib/ native runtime drop-in directory (create as needed)
├─ docs/ math and native-core notes in both languages
├─ requirements.txt Python dependency
├─ README.md Chinese documentation
└─ README.en.md English documentation
```
## Mathematical implementation
The main documented stages are:
- dense JOC differential reconstruction and dequantization;
- parameter-band mapping to 64 QMF subbands;
- cross-frame parameter interpolation;
- analysis/inverse QMF, surround delay, and FIR state;
- the 1217-sample LFE delay;
- OAMD Q15 coordinate conversion;
- equal-power panning over target-layout regions;
- layout-dependent position compensation and sample-wise gain ramps;
- float32 and PCM24 output quantization.
See the [mathematical notes](docs/math.en.md) for the equations used by the decoding and rendering process.
## Known limitations
- Only the common contiguous EMDF transport is covered. Fragmented transport across multiple audio-block skip fields is not covered.
- Dense JOC is the main path. The Sparse JOC branch should not be treated as supported.
- The speaker path currently covers ordinary point objects; extent, spread, divergence, and similar modes are outside the supported scope.
- Multi-data-point streams, uncommon band configurations, and unusual OAMD scheduling have less coverage than common 12-band, single-data-point material.
- A speaker limiter is outside the current primary formula.
- ADM output, native binaries, and speaker layouts still need broader interoperability checks across platforms, players, and real material.
## Documentation
- [Mathematical notes](docs/math.en.md) · [中文](docs/math.md)
- [Native-core notes](docs/native.en.md) · [中文](docs/native.md)
+167
View File
@@ -0,0 +1,167 @@
# JustOneCacophony — JOC
[English](README.en.md)
> JustOneCacophony 是一个 E-AC-3 JOC 的实验性 / 测试实现,用于研究 JOC 的解析、重建、渲染以及相关数学过程。
项目可以从常见 E-AC-3 JOC 码流中提取并解析 EMDF、ID14 JOC 参数和 ID11 OAMD 元数据,结合 FFmpeg 解码出的核心 5.1 PCM 重建 LFE 与 15 路对象 PCM,并输出 ADM BWF 或指定扬声器布局的 WAV。
这是研究代码,不是完整、标准兼容或生产级的 Dolby JOC 解码器。它只覆盖当前已实现的码流形态;遇到未知变体时会明确报错,而不是假装一切都很和谐——如果哪里算错了,它可能就真的只剩 cacophony 了。
## 当前功能
- 扫描 E-AC-3 同步帧中的常见连续 EMDF 容器;
- 解析 ID14 dense JOC 参数、Huffman 数据、差分矩阵与 `joc_clipgain`;
- 解析 ID11 OAMD 位置更新并生成对象轨迹;
- 通过 analysis QMF、参数插值、对象矩阵和 inverse QMF 重建 LFE + 15 路对象 PCM;
- 输出 25 声道 ADM BWF:10 声道 7.1.2 bed(除 LFE 外静音)+ 15 个对象;
- 直接渲染 `2.0`、`3.1`、`5.1`、`7.1`、`5.1.2`、`5.1.4`、`7.1.2`、`7.1.4`、`9.1.4`、`9.1.6`;
- 输出 float32 或 PCM24 WAV,并在 PCM24 削波前提供明确处理策略;
- 使用 NumPy 后端,或通过 `ctypes` 调用可选的 C++20 原生核;`auto` 模式在原生库不可用时回退到 Python;
- 读取或写入 metadata sidecar,并生成元数据、运行时间和输出摘要。
## 处理流程
```text
M4A / E-AC-3
├─ FFmpeg 提取 E-AC-3 并解码核心 5.1 PCM
├─ EMDF → ID14 JOC 参数 → 对象矩阵
├─ 核心 PCM → analysis QMF → 参数插值 → inverse QMF
├─ ID11 OAMD → 对象位置与时间轨迹
└─ LFE + 15 objects
├─ 25ch ADM BWF
└─ 指定布局的扬声器 WAV
```
Python 与 C++ 后端使用同一组已记录的数学过程。原生核只处理状态密集的 DSP 和扬声器渲染,高层位流解析、ADM 组装与命令行逻辑仍在 Python 中。
## 环境
- Python 3.10+
- NumPy 1.24+
- 独立的 FFmpeg 可执行程序;不需要 `ffmpeg-python`。默认从 `PATH` 查找,也可通过 `--ffmpeg` 指定可执行文件路径
- 可选:支持 C++20 的 CMake 工具链,用于自行构建原生核
建议在项目专用虚拟环境中安装依赖:
```powershell
python -m pip install -r requirements.txt
```
如果 FFmpeg 不在 `PATH` 中:
```powershell
python main.py input.m4a --ffmpeg C:\path\to\ffmpeg.exe
```
## 使用方法
默认输出 25 声道 ADM BWF:
```powershell
python main.py input.m4a
```
选择后端或输出路径:
```powershell
python main.py input.eac3 -o output.adm.wav --backend python
python main.py input.m4a --backend native --native-threads 2
python main.py input.m4a --native-library lib/eac3joc_core.dll
```
直接输出扬声器 WAV:
```powershell
python main.py input.m4a --speaker-layout 2.0 --speaker-format float32
python main.py input.m4a --speaker-layout 5.1 --speaker-format int24
python main.py input.m4a --speaker-layout 7.1.2 --speaker-output output.7.1.2.wav
```
在非交互环境请求 PCM24 且可能削波时,需要显式选择处理方式:
```powershell
python main.py input.m4a --speaker-layout 5.1 --speaker-format int24 --clip-action abort
python main.py input.m4a --speaker-layout 5.1 --speaker-format int24 --clip-action float32
python main.py input.m4a --speaker-layout 5.1 --speaker-format int24 --clip-action continue
```
元数据与诊断:
```powershell
python main.py input.m4a --print-metadata summary
python main.py input.m4a --metadata-only --print-metadata frames
python main.py input.m4a --metadata-cache metadata_cache
python main.py input.m4a --metadata-dir metadata_cache
```
更多参数可查看:
```powershell
python main.py --help
```
未指定 `-o` 时,输出仍写入仓库根目录的 `output/`。这是文件移动后特意保持的原有行为。
## 原生核
仓库默认不附带原生二进制。可以从项目 Release 下载适合当前平台的预构建运行库,或自行构建,然后把运行库直接放入仓库根目录的 `lib/`;若该目录不存在,创建即可。自行构建时可从仓库根目录使用 CMake:
```powershell
cmake -S native -B build/cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$PWD/lib"
cmake --build build/cmake --config Release
cmake --install build/cmake --config Release
```
运行时查找顺序为:
1. `--native-library`;
2. `EAC3JOC_NATIVE_LIBRARY`;
3. `lib/` 下当前平台的标准库文件名。
详细 ABI、状态与精度说明见[原生核说明](docs/native.md)。
## 目录结构
```text
JustOneCacophony/
├─ main.py 命令行启动入口
├─ src/ Python 实现模块
├─ native/ C/C++ 加速核、C ABI 与必要表数据
├─ data/ Python 运行时表数据
├─ lib/ 原生运行库投放目录(按需创建)
├─ docs/ 数学与原生核文档(中英文)
├─ requirements.txt Python 依赖
├─ README.md 中文说明
└─ README.en.md English documentation
```
## 数学实现
核心过程包括:
- dense JOC 差分还原与去量化;
- 参数带到 64 个 QMF 子带的映射;
- 跨帧参数插值;
- analysis / inverse QMF、环绕声道延迟与 FIR 状态;
- LFE 1217-sample 延迟;
- OAMD Q15 坐标转换;
- 基于目标布局 region 的等功率声像;
- 布局位置补偿与逐样本增益斜坡;
- float32 与 PCM24 输出量化。
解码与渲染过程使用的公式见[数学说明](docs/math.md)。
## 已知限制
- 当前只覆盖常见 continuous EMDF transport;跨多个 audio-block skip field 的碎片化 transport 尚未覆盖。
- Dense JOC 是当前主要路径;Sparse JOC 分支不应视为受支持能力。
- 扬声器路径当前只覆盖普通点对象;extent、spread、divergence 等对象模式不在支持范围内。
- 多数据点、少见参数带配置和特殊 OAMD 调度的覆盖度低于常见 12-band、单数据点素材。
- 扬声器 limiter 不属于当前实现的主公式。
- ADM 输出、原生库和扬声器布局仍需在更多平台、播放器与真实素材上确认互操作性。
## 文档
- [数学说明](docs/math.md) · [English](docs/math.en.md)
- [原生核说明](docs/native.md) · [English](docs/native.en.md)
+20
View File
@@ -0,0 +1,20 @@
# Python runtime tables
[中文](README.md)
`tables.npz` contains the static table data used by the Python path:
```text
analysis_window float64[10,64]
qmf5_window float64[640]
joc_huff_code_coarse_generic int64[95,2]
joc_huff_code_fine_generic int64[191,2]
joc_huff_code_coarse_coeff_sparse int64[95,2]
joc_huff_code_fine_coeff_sparse int64[191,2]
joc_huff_code_5ch_pos_index_sparse int64[4,2]
joc_huff_code_7ch_pos_index_sparse int64[6,2]
```
`src/joc_qmf.py` loads the QMF tables, while `src/joc_decode.py` loads the JOC Huffman trees. Python does not read C/C++ headers under `native/`.
The corresponding native data are stored in `native/src/qmf_tables.h` and `native/src/joc_huffman_tables.h`. Changes on either side should update the other and be checked for value-by-value agreement.
+20
View File
@@ -0,0 +1,20 @@
# Python 运行时表
[English](README.en.md)
`tables.npz` 集中保存 Python 路径使用的静态表数据:
```text
analysis_window float64[10,64]
qmf5_window float64[640]
joc_huff_code_coarse_generic int64[95,2]
joc_huff_code_fine_generic int64[191,2]
joc_huff_code_coarse_coeff_sparse int64[95,2]
joc_huff_code_fine_coeff_sparse int64[191,2]
joc_huff_code_5ch_pos_index_sparse int64[4,2]
joc_huff_code_7ch_pos_index_sparse int64[6,2]
```
`src/joc_qmf.py` 读取 QMF 表,`src/joc_decode.py` 读取 JOC Huffman 树。Python 不读取 `native/` 下的 C/C++ 头文件。
原生侧对应数据分别位于 `native/src/qmf_tables.h` 与 `native/src/joc_huffman_tables.h`。修改任何一侧时,应同步更新另一侧并进行逐值一致性检查。
BIN
View File
Binary file not shown.
+578
View File
@@ -0,0 +1,578 @@
# JustOneCacophony — E-AC-3 JOC decoding and rendering mathematics
[中文](math.md) · [Back to README](../README.en.md)
This document covers only the signal model and formulas used in the JustOneCacophony research path: how JOC parameters combine with core PCM to reconstruct object signals, and how OAMD coordinates become speaker gains.
The formulas describe the dense-JOC and ordinary point-object paths studied by the project. They are not a complete definition of every E-AC-3 JOC variant.
## 1. Overall path and notation
Object reconstruction:
```text
E-AC-3 core 5.1 PCM
+ ID14 JOC matrix parameters
→ analysis QMF
→ parameter-band expansion and time interpolation
→ object matrix
→ inverse QMF
→ LFE + 15 object PCM channels
```
Speaker rendering:
```text
LFE + 15 object PCM channels
+ ID11 OAMD coordinates and update timing
→ target-layout region
→ equal-power panning
→ position compensation
→ sample-wise gain ramp
→ speaker PCM
```
Main notation:
| Symbol | Meaning |
|---|---|
| $c=0\ldots4$ | core channels L, R, C, Ls, Rs |
| $o=0\ldots14$ | 15 JOC objects |
| $b=0\ldots63$ | complex QMF subbands |
| $t=0\ldots23$ | 24 64-sample slots per frame |
| $p(b)$ | JOC parameter band corresponding to QMF subband $b$ |
| $X_{c,b,t}$ | analysis-QMF value for a core channel |
| $M_{o,c,b,t}$ | object-matrix coefficient |
| $Z_{o,b,t}$ | inverse-QMF input for an object |
| $y_o[n]$ | time-domain object PCM |
The number of samples in one frame is
$$
N_f=1536=24\times64.
$$
## 2. Dense-JOC matrix parameters
### 2.1 Differential reconstruction
Let `quant_idx` be $q_i\in\{0,1\}$. The number of quantization levels is
$$
N_q=
\begin{cases}
96, & q_i=0,\\
192, & q_i=1.
\end{cases}
$$
The center offset is
$$
O_q=\frac{N_q}{2}.
$$
For object $o$, data point $d$, core channel $c$, and parameter band $p$, the coded difference $\Delta_{o,d,c,p}$ reconstructs to
$$
Q_{o,d,c,0}
=
\left(O_q+\Delta_{o,d,c,0}\right)\bmod N_q,
$$
$$
Q_{o,d,c,p}
=
\left(Q_{o,d,c,p-1}+\Delta_{o,d,c,p}\right)\bmod N_q,
\qquad p>0.
$$
### 2.2 Dequantization
The dequantized matrix coefficient is
$$
D_{o,d,c,p}
=
\left(Q_{o,d,c,p}-\frac{N_q}{2}\right)
\frac{820}{4096(1+q_i)}.
$$
The effective denominator is therefore 4096 in coarse mode and 8192 in fine mode.
### 2.3 JOC clipgain
If the clipgain field consists of integer $x$ and mantissa $y$, then
$$
G_{\mathrm{clip}}
=
1+\frac{y}{32}2^{x-4}.
$$
It is applied to object PCM after inverse QMF and does not apply to LFE.
## 3. Parameter-band expansion and time interpolation
### 3.1 Parameter bands to QMF subbands
The JOC matrix is coded in parameter bands, while the QMF contains 64 subbands. Let $p(b)$ identify the parameter band containing subband $b$. A parameter-band coefficient expands as
$$
D_{o,d,c,b}=D_{o,d,c,p(b)}.
$$
The common 12-band mapping is
$$
\begin{aligned}
\mathcal B_0 &= \{0\}, &
\mathcal B_1 &= \{1\}, &
\mathcal B_2 &= \{2\}, &
\mathcal B_3 &= \{3\},\\
\mathcal B_4 &= \{4,5\}, &
\mathcal B_5 &= \{6,7\}, &
\mathcal B_6 &= \{8,9,10\}, &
\mathcal B_7 &= \{11,12,13\},\\
\mathcal B_8 &= \{14,15,16,17\}, &
\mathcal B_9 &= \{18,\ldots,22\},\\
\mathcal B_{10} &= \{23,\ldots,34\}, &
\mathcal B_{11} &= \{35,\ldots,63\}.
\end{aligned}
$$
Thus $p(b)=k$ if and only if $b\in\mathcal B_k$. Other parameter-band counts use their corresponding subband boundaries.
### 3.2 One-data-point interpolation
Let $P_{o,c,b}$ be the previous frame-end value and $D_{o,c,p(b)}$ the current target. For slot $t=0\ldots23$:
$$
\alpha_t=\frac{t+1}{24},
$$
$$
M_{o,c,b,t}
=
(1-\alpha_t)P_{o,c,b}
+\alpha_tD_{o,c,p(b)}.
$$
The first slot has therefore advanced by $1/24$ of the ramp, while the last slot equals the current target:
$$
M_{o,c,b,23}=D_{o,c,p(b)}.
$$
This value then becomes the previous state for the next frame.
### 3.3 Multiple data points
When a frame contains two data points, `offset_ts` gives the segment boundary. Each segment uses the same linear relation between the previous and next targets; step mode switches targets at the designated slot.
## 4. Analysis QMF for core PCM
The matrix input uses core channels L, R, C, Ls, and Rs; LFE follows a separate path. Core PCM is first scaled as
$$
\widetilde x_c[n]=\frac{x_c[n]}{16}.
$$
Let $\mathcal A_b$ denote the 64-band analysis-QMF operator with polyphase history state. Then
$$
X_{c,b,t}
=
\mathcal A_b\!\left(
\widetilde x_c[64t],\ldots,\widetilde x_c[64t+63];
\mathbf s^{\mathrm A}_{c,t}
\right).
$$
This consists of the analysis window/polyphase stage, modulation, a 64-point FFT, and subband reordering. History state advances continuously across slots and frames.
## 5. QMF-domain processing of core channels
L, R, and C are delayed by ten QMF slots before entering the object matrix:
$$
\widehat X_{c,b,t}=X_{c,b,t-10},
\qquad c\in\{L,R,C\}.
$$
Ls and Rs use the same ten-slot delay and a $-j$ rotation for $b>0$:
$$
\widehat X_{c,b,t}=-jX_{c,b,t-10},
\qquad c\in\{Ls,Rs\},\ b>0.
$$
Band 0 of each surround channel additionally passes through a 21-tap complex FIR:
$$
\widehat X_{c,0,t}
=
\sum_{k=0}^{20}h_kX_{c,0,t-k}.
$$
These delays and filter histories are decoder state and cannot be reset independently for every frame.
## 6. Object matrix
For each object $o$, subband $b$, and slot $t$, the object's frequency-domain value is a linear combination of the five core channels:
$$
Z_{o,b,t}
=
\sum_{c=0}^{4}
M_{o,c,b,t}\widehat X_{c,b,t}.
$$
The $1/16$ analysis-input scale is canceled by the $\times16$ factor after inverse QMF, so the matrix itself needs no additional empirical gain.
## 7. Object inverse QMF
### 7.1 Subband reorder
Write the 64 complex subbands as 128 interleaved real values in `src`. For $k=0\ldots31$:
$$
\begin{aligned}
\operatorname{zone}[2k] &= \operatorname{src}[4k],\\
\operatorname{zone}[2k+1] &= -\operatorname{src}[4k+1],\\
\operatorname{zone}[126-2k] &= \operatorname{src}[4k+2],\\
\operatorname{zone}[127-2k] &= \operatorname{src}[4k+3].
\end{aligned}
$$
Treat `zone` as 64 complex values and apply an unnormalized 64-point FFT:
$$
F_k
=
\sum_{n=0}^{63}
\operatorname{zone}_n
\exp\!\left(-j\frac{2\pi kn}{64}\right).
$$
### 7.2 Modulation and synthesis
Define the rotation coefficient
$$
r_k
=
\frac12\left(
\sin\frac{\pi k}{128}
+j\cos\frac{\pi k}{128}
\right),
$$
and compute
$$
R_k=2F_kr_k.
$$
Let $\mathcal S$ denote polyphase synthesis with a 640-value synthesis window and cross-slot state:
$$
\mathbf y_{o,t}
=
\mathcal S\!\left(
\mathbf R_{o,t},W,\mathbf s^{\mathrm S}_{o,t}
\right).
$$
Object output is
$$
y_o[64t+r]
=
\operatorname{clip}\!\left(
16\,\mathbf y_{o,t}[r],-1,1
\right)G_{\mathrm{clip}},
$$
where $r=0\ldots63$. Synthesis state must advance continuously by slot.
## 8. LFE path
LFE bypasses the object matrix and inverse QMF and uses a 1217-sample delay. After the input and output scale factors cancel:
$$
y_{\mathrm{LFE}}[n]
=
\operatorname{clip}\!\left(
x_{\mathrm{LFE,core}}[n-1217],-1,1
\right).
$$
## 9. OAMD coordinates
The lateral and longitudinal grids use $N=62$; the height grid uses $N=15$. The quantizer is
$$
q_N(k)
=
\min\!\left(
32767,
\left\lfloor\frac{32768k}{N}+\frac12\right\rfloor
\right).
$$
OAR coordinates are
$$
u=\frac{q_1}{32768},
\qquad
v=\frac{q_2}{32768},
\qquad
w=\frac{q_3}{32768}.
$$
Their maximum runtime value is $32767/32768$, not exactly 1.
For conversion to the ADM grid:
$$
k_1=\operatorname{round}\!\left(\frac{62q_1}{32767}\right),
\quad
k_2=\operatorname{round}\!\left(\frac{62q_2}{32767}\right),
\quad
k_3=\operatorname{round}\!\left(\frac{15q_3}{32767}\right),
$$
$$
X=2\frac{k_1}{62}-1,
\qquad
Y=1-2\frac{k_2}{62},
\qquad
Z=\frac{k_3}{15}.
$$
The continuous-coordinate relation is
$$
u=\frac{X+1}{2},
\qquad
v=\frac{1-Y}{2},
\qquad
w=Z.
$$
## 10. Equal-power speaker panning
### 10.1 One-dimensional interpolation
Let adjacent speaker coordinates be $a_0<a_1$ and object position be $a$. The normalized position is
$$
\tau=\frac{a-a_0}{a_1-a_0}.
$$
Gains inside the interval are
$$
g_0(\tau)=\cos\left(\frac\pi2\tau\right),
\qquad
g_1(\tau)=\sin\left(\frac\pi2\tau\right),
$$
and satisfy
$$
g_0^2(\tau)+g_1^2(\tau)=1.
$$
Object positions outside the interval are clamped to the nearest endpoint.
### 10.2 Two-dimensional regions
Each row first produces a horizontal gain vector $\mathbf h_r(u)$. If the object lies between adjacent rows $r_0,r_1$:
$$
\eta=\frac{v-v_{r_0}}{v_{r_1}-v_{r_0}},
$$
$$
a_0=\cos\left(\frac\pi2\eta\right),
\qquad
a_1=\sin\left(\frac\pi2\eta\right).
$$
The two-dimensional point gain is
$$
\mathbf G_{\mathrm{2D}}(u,v)
=
\mathbf h(u)\odot\mathbf v(v).
$$
For 5.1-family layouts with one horizontal surround pair rather than separate side and rear pairs, the longitudinal coordinate is
$$
v_{\mathrm{floor}}
=
\operatorname{clamp}(2v,0,1).
$$
Other layouts use $v_{\mathrm{floor}}=v$.
### 10.3 Height layer
Three-dimensional layouts compute floor gain $\mathbf G_f$ and height gain $\mathbf G_h$ separately:
$$
\mathbf G_{\mathrm{point}}(u,v,w)
=
\cos\left(\frac\pi2w\right)\mathbf G_f
+
\sin\left(\frac\pi2w\right)\mathbf G_h.
$$
When the floor and height speaker sets do not overlap and each layer uses equal-power interpolation:
$$
\left\|\mathbf G_{\mathrm{point}}\right\|_2=1.
$$
## 11. Layout-dependent position compensation
Let $N_h$ be the number of relevant height speakers and $N_f$ the number of relevant additional horizontal speakers:
$$
H=\min\left(\frac{N_h}{4},1\right),
\qquad
F=\min\left(\frac{N_f}{4},1\right).
$$
Maximum position compensation is
$$
A_{\max}
=
-\max\left(4.5-1.5H-3F,0\right)
\quad\text{dB}.
$$
Longitudinal and height weights are
$$
p_v=\operatorname{clamp}\left(\frac v{0.6},0,1\right),
$$
$$
p_w=\operatorname{clamp}\left(\frac{w-0.2}{0.8},0,1\right),
$$
$$
p=\operatorname{clamp}(p_v+p_w,0,1).
$$
The linear compensation gain is
$$
G_{\mathrm{pos}}=10^{A_{\max}p/20}.
$$
The object's target-gain vector is
$$
\mathbf G_{\mathrm{target}}
=
G_{\mathrm{object}}
G_{\mathrm{pos}}
\mathbf G_{\mathrm{point}}.
$$
## 12. OAMD time alignment and gain ramps
The coded position of an OAMD update is
$$
s_{\mathrm{coded}}
=
s_{\mathrm{frame}}
+s_{\mathrm{outer}}
+s_{\mathrm{OAMD}}
+32f_{\mathrm{block}}.
$$
For processing-block length $B=32$, the aligned update point is
$$
\widehat s
=
B\left\lfloor
\frac{s_{\mathrm{coded}}+B/2-1}{B}
\right\rfloor.
$$
For ramp duration $D$, the number of blocks is
$$
K
=
\left\lfloor
\frac{D+B/2-1}{B}
\right\rfloor.
$$
If current gain is $g_0$ and target gain is $g_1$, the increment per block is
$$
\Delta g=\frac{g_1-g_0}{K}.
$$
Sample $r=0\ldots B-1$ of block $j$ uses
$$
g_{j,r}=g_j+\frac rB\Delta g,
\qquad
g_{j+1}=g_j+\Delta g.
$$
If no new metadata update intervenes, this is equivalent to a sample-wise linear ramp of total length $KB$.
## 13. Final speaker mix
For target output channel $c$:
$$
y_c[n]
=
\delta_{c,\mathrm{LFE}}x_{\mathrm{LFE}}[n]
+
\sum_{o=1}^{15}x_o[n]g_{o,c}[n].
$$
Here
$$
\delta_{c,\mathrm{LFE}}
=
\begin{cases}
1, & c\text{ is the target layout's LFE channel},\\
0, & \text{otherwise}.
\end{cases}
$$
A layout without LFE output does not mix input LFE into other channels. After object accumulation, output channels are ordered as required by the target format.
For PCM24 output, quantization is
$$
y_{24}[n]
=
\operatorname{trunc}\left(
8388607\,\operatorname{clip}(y[n],-1,1)
\right).
$$
## 14. Scope of the formulas
- The JOC matrix section describes dense JOC; Sparse JOC uses a different sparse coefficient/index path.
- The speaker-panning section describes ordinary point objects; extent, spread, divergence, and similar modes require additional models.
- Multiple OAMD position blocks must be scheduled in time order.
- A limiter is separate post-processing and is not included in the mixing equations above.
+578
View File
@@ -0,0 +1,578 @@
# JustOneCacophony — E-AC-3 JOC 解码与渲染数学
[English](math.en.md) · [返回 README](../README.md)
本文只说明 JustOneCacophony 研究路径中使用的信号模型和公式:JOC 参数如何与核心 PCM 结合并重建对象信号,以及 OAMD 坐标如何转换为扬声器增益。
这些公式描述项目当前研究的 dense JOC 与普通点对象路径,不代表对所有 E-AC-3 JOC 变体的完整定义。
## 1. 总体路径与记号
对象重建路径:
```text
E-AC-3 核心 5.1 PCM
+ ID14 JOC 矩阵参数
→ analysis QMF
→ 参数带展开与时间插值
→ 对象矩阵
→ inverse QMF
→ LFE + 15 路对象 PCM
```
扬声器渲染路径:
```text
LFE + 15 路对象 PCM
+ ID11 OAMD 坐标与更新时间
→ 目标布局 region
→ 等功率声像
→ 位置补偿
→ 逐样本增益斜坡
→ 扬声器 PCM
```
主要记号:
| 符号 | 含义 |
|---|---|
| $c=0\ldots4$ | 核心声道 L、R、C、Ls、Rs |
| $o=0\ldots14$ | 15 个 JOC 对象 |
| $b=0\ldots63$ | 复 QMF 子带 |
| $t=0\ldots23$ | 每帧 24 个 64-sample 时槽 |
| $p(b)$ | QMF 子带 $b$ 对应的 JOC 参数带 |
| $X_{c,b,t}$ | 核心声道的 analysis-QMF 值 |
| $M_{o,c,b,t}$ | 对象矩阵系数 |
| $Z_{o,b,t}$ | 对象的 inverse-QMF 输入 |
| $y_o[n]$ | 对象时域 PCM |
一帧的采样数为
$$
N_f=1536=24\times64.
$$
## 2. Dense JOC 矩阵参数
### 2.1 差分还原
令 `quant_idx` 为 $q_i\in\{0,1\}$,量化级数为
$$
N_q=
\begin{cases}
96, & q_i=0,\\
192, & q_i=1.
\end{cases}
$$
中心偏移为
$$
O_q=\frac{N_q}{2}.
$$
对对象 $o$、数据点 $d$、核心声道 $c$ 和参数带 $p$,编码差分 $\Delta_{o,d,c,p}$ 还原为
$$
Q_{o,d,c,0}
=
\left(O_q+\Delta_{o,d,c,0}\right)\bmod N_q,
$$
$$
Q_{o,d,c,p}
=
\left(Q_{o,d,c,p-1}+\Delta_{o,d,c,p}\right)\bmod N_q,
\qquad p>0.
$$
### 2.2 去量化
矩阵系数的去量化值为
$$
D_{o,d,c,p}
=
\left(Q_{o,d,c,p}-\frac{N_q}{2}\right)
\frac{820}{4096(1+q_i)}.
$$
因此 coarse 模式的有效分母为 4096,fine 模式为 8192。
### 2.3 JOC clipgain
若 clipgain 字段由整数 $x$ 和尾数 $y$ 组成,则
$$
G_{\mathrm{clip}}
=
1+\frac{y}{32}2^{x-4}.
$$
它在对象 inverse QMF 之后作用于对象 PCM,不作用于 LFE。
## 3. 参数带展开与时间插值
### 3.1 参数带到 QMF 子带
JOC 矩阵按参数带编码,而 QMF 使用 64 个子带。令 $p(b)$ 表示子带 $b$ 所属的参数带,则每个参数带系数展开为
$$
D_{o,d,c,b}=D_{o,d,c,p(b)}.
$$
常见的 12-band 映射为
$$
\begin{aligned}
\mathcal B_0 &= \{0\}, &
\mathcal B_1 &= \{1\}, &
\mathcal B_2 &= \{2\}, &
\mathcal B_3 &= \{3\},\\
\mathcal B_4 &= \{4,5\}, &
\mathcal B_5 &= \{6,7\}, &
\mathcal B_6 &= \{8,9,10\}, &
\mathcal B_7 &= \{11,12,13\},\\
\mathcal B_8 &= \{14,15,16,17\}, &
\mathcal B_9 &= \{18,\ldots,22\},\\
\mathcal B_{10} &= \{23,\ldots,34\}, &
\mathcal B_{11} &= \{35,\ldots,63\}.
\end{aligned}
$$
其中 $p(b)=k$ 当且仅当 $b\in\mathcal B_k$。其他参数带数使用各自的子带边界。
### 3.2 单数据点插值
令上一帧末值为 $P_{o,c,b}$,当前目标值为 $D_{o,c,p(b)}$。对时槽 $t=0\ldots23$:
$$
\alpha_t=\frac{t+1}{24},
$$
$$
M_{o,c,b,t}
=
(1-\alpha_t)P_{o,c,b}
+\alpha_tD_{o,c,p(b)}.
$$
因此第一时槽已经推进 ramp 的 $1/24$,最后一时槽等于当前目标:
$$
M_{o,c,b,23}=D_{o,c,p(b)}.
$$
该值随后成为下一帧的 previous 状态。
### 3.3 多数据点
当一帧含两个数据点时,`offset_ts` 给出分段边界。每一段在上一目标和下一目标之间使用相同的线性关系;阶跃模式则在指定时槽直接切换目标。
## 4. 核心 PCM 的 analysis QMF
矩阵输入使用核心声道 L、R、C、Ls、Rs;LFE 走独立路径。核心 PCM 先缩放为
$$
\widetilde x_c[n]=\frac{x_c[n]}{16}.
$$
令 $\mathcal A_b$ 表示带 polyphase 历史状态的 64-band analysis-QMF 算子,则
$$
X_{c,b,t}
=
\mathcal A_b\!\left(
\widetilde x_c[64t],\ldots,\widetilde x_c[64t+63];
\mathbf s^{\mathrm A}_{c,t}
\right).
$$
该过程依次包含 analysis window/polyphase、调制、64 点 FFT 和子带重排。历史状态跨时槽和帧连续推进。
## 5. 核心声道的 QMF 域处理
L、R、C 在进入对象矩阵前延迟 10 个 QMF 时槽:
$$
\widehat X_{c,b,t}=X_{c,b,t-10},
\qquad c\in\{L,R,C\}.
$$
Ls、Rs 同样延迟 10 个时槽,并在 $b>0$ 时作 $-j$ 旋转:
$$
\widehat X_{c,b,t}=-jX_{c,b,t-10},
\qquad c\in\{Ls,Rs\},\ b>0.
$$
环绕声道的 band 0 还经过 21-tap 复 FIR:
$$
\widehat X_{c,0,t}
=
\sum_{k=0}^{20}h_kX_{c,0,t-k}.
$$
这些延迟和滤波历史属于解码状态,不能按帧独立清零。
## 6. 对象矩阵
对每个对象 $o$、子带 $b$ 和时槽 $t$,对象频域值为五个核心声道的线性组合:
$$
Z_{o,b,t}
=
\sum_{c=0}^{4}
M_{o,c,b,t}\widehat X_{c,b,t}.
$$
analysis 输入的 $1/16$ 缩放会在 inverse QMF 输出端由 $\times16$ 抵消,因此矩阵本身不需要额外经验增益。
## 7. 对象 inverse QMF
### 7.1 子带重排
将 64 个复子带写成 128 个交织实数 `src`。对 $k=0\ldots31$:
$$
\begin{aligned}
\operatorname{zone}[2k] &= \operatorname{src}[4k],\\
\operatorname{zone}[2k+1] &= -\operatorname{src}[4k+1],\\
\operatorname{zone}[126-2k] &= \operatorname{src}[4k+2],\\
\operatorname{zone}[127-2k] &= \operatorname{src}[4k+3].
\end{aligned}
$$
把 `zone` 重新视为 64 个复数后执行未归一化 64 点 FFT:
$$
F_k
=
\sum_{n=0}^{63}
\operatorname{zone}_n
\exp\!\left(-j\frac{2\pi kn}{64}\right).
$$
### 7.2 调制与合成
定义旋转系数
$$
r_k
=
\frac12\left(
\sin\frac{\pi k}{128}
+j\cos\frac{\pi k}{128}
\right),
$$
并计算
$$
R_k=2F_kr_k.
$$
令 $\mathcal S$ 表示带 640 项 synthesis window 和跨时槽状态的 polyphase 合成算子:
$$
\mathbf y_{o,t}
=
\mathcal S\!\left(
\mathbf R_{o,t},W,\mathbf s^{\mathrm S}_{o,t}
\right).
$$
对象输出为
$$
y_o[64t+r]
=
\operatorname{clip}\!\left(
16\,\mathbf y_{o,t}[r],-1,1
\right)G_{\mathrm{clip}},
$$
其中 $r=0\ldots63$。synthesis 状态必须按时槽连续推进。
## 8. LFE 路径
LFE 不经过对象矩阵或 inverse QMF,而是使用 1217-sample 延迟。输入与输出端的比例因子抵消后:
$$
y_{\mathrm{LFE}}[n]
=
\operatorname{clip}\!\left(
x_{\mathrm{LFE,core}}[n-1217],-1,1
\right).
$$
## 9. OAMD 坐标
横向和纵向网格使用 $N=62$,高度网格使用 $N=15$。量化函数为
$$
q_N(k)
=
\min\!\left(
32767,
\left\lfloor\frac{32768k}{N}+\frac12\right\rfloor
\right).
$$
OAR 坐标为
$$
u=\frac{q_1}{32768},
\qquad
v=\frac{q_2}{32768},
\qquad
w=\frac{q_3}{32768}.
$$
其最大运行值为 $32767/32768$,不是精确的 1。
转换为 ADM 网格时:
$$
k_1=\operatorname{round}\!\left(\frac{62q_1}{32767}\right),
\quad
k_2=\operatorname{round}\!\left(\frac{62q_2}{32767}\right),
\quad
k_3=\operatorname{round}\!\left(\frac{15q_3}{32767}\right),
$$
$$
X=2\frac{k_1}{62}-1,
\qquad
Y=1-2\frac{k_2}{62},
\qquad
Z=\frac{k_3}{15}.
$$
连续坐标关系为
$$
u=\frac{X+1}{2},
\qquad
v=\frac{1-Y}{2},
\qquad
w=Z.
$$
## 10. 等功率扬声器声像
### 10.1 一维插值
相邻扬声器坐标为 $a_0<a_1$,对象位置为 $a$。归一化位置为
$$
\tau=\frac{a-a_0}{a_1-a_0}.
$$
区间内的增益为
$$
g_0(\tau)=\cos\left(\frac\pi2\tau\right),
\qquad
g_1(\tau)=\sin\left(\frac\pi2\tau\right),
$$
并满足
$$
g_0^2(\tau)+g_1^2(\tau)=1.
$$
区间外的对象位置夹到最近端点。
### 10.2 二维 region
每一行先沿 $u$ 得到横向增益向量 $\mathbf h_r(u)$。若对象位于相邻两行 $r_0,r_1$ 之间:
$$
\eta=\frac{v-v_{r_0}}{v_{r_1}-v_{r_0}},
$$
$$
a_0=\cos\left(\frac\pi2\eta\right),
\qquad
a_1=\sin\left(\frac\pi2\eta\right).
$$
二维点增益为
$$
\mathbf G_{\mathrm{2D}}(u,v)
=
\mathbf h(u)\odot\mathbf v(v).
$$
对于只有一对水平环绕、没有独立 side/rear 两对的 5.1 系列布局,纵向坐标使用
$$
v_{\mathrm{floor}}
=
\operatorname{clamp}(2v,0,1).
$$
其他布局使用 $v_{\mathrm{floor}}=v$。
### 10.3 高度层
三维布局分别计算地面层增益 $\mathbf G_f$ 和高度层增益 $\mathbf G_h$:
$$
\mathbf G_{\mathrm{point}}(u,v,w)
=
\cos\left(\frac\pi2w\right)\mathbf G_f
+
\sin\left(\frac\pi2w\right)\mathbf G_h.
$$
当地面层与高度层扬声器集合不重叠、且各层内部使用等功率插值时:
$$
\left\|\mathbf G_{\mathrm{point}}\right\|_2=1.
$$
## 11. 布局位置补偿
令 $N_h$ 为相关高度扬声器数,$N_f$ 为相关附加水平扬声器数:
$$
H=\min\left(\frac{N_h}{4},1\right),
\qquad
F=\min\left(\frac{N_f}{4},1\right).
$$
最大位置补偿为
$$
A_{\max}
=
-\max\left(4.5-1.5H-3F,0\right)
\quad\text{dB}.
$$
前后与高度位置权重为
$$
p_v=\operatorname{clamp}\left(\frac v{0.6},0,1\right),
$$
$$
p_w=\operatorname{clamp}\left(\frac{w-0.2}{0.8},0,1\right),
$$
$$
p=\operatorname{clamp}(p_v+p_w,0,1).
$$
线性补偿增益为
$$
G_{\mathrm{pos}}=10^{A_{\max}p/20}.
$$
对象的目标增益向量为
$$
\mathbf G_{\mathrm{target}}
=
G_{\mathrm{object}}
G_{\mathrm{pos}}
\mathbf G_{\mathrm{point}}.
$$
## 12. OAMD 时间对齐与增益斜坡
OAMD 更新的编码位置为
$$
s_{\mathrm{coded}}
=
s_{\mathrm{frame}}
+s_{\mathrm{outer}}
+s_{\mathrm{OAMD}}
+32f_{\mathrm{block}}.
$$
对处理块长度 $B=32$,更新点对齐为
$$
\widehat s
=
B\left\lfloor
\frac{s_{\mathrm{coded}}+B/2-1}{B}
\right\rfloor.
$$
给定 ramp duration $D$,block 数为
$$
K
=
\left\lfloor
\frac{D+B/2-1}{B}
\right\rfloor.
$$
若当前增益为 $g_0$、目标为 $g_1$,则每 block 的增量为
$$
\Delta g=\frac{g_1-g_0}{K}.
$$
第 $j$ 个 block 内的样本 $r=0\ldots B-1$ 使用
$$
g_{j,r}=g_j+\frac rB\Delta g,
\qquad
g_{j+1}=g_j+\Delta g.
$$
如果中途没有新的 metadata 更新,该过程等价于总长度 $KB$ 的逐样本线性斜坡。
## 13. 最终扬声器混音
对目标输出声道 $c$:
$$
y_c[n]
=
\delta_{c,\mathrm{LFE}}x_{\mathrm{LFE}}[n]
+
\sum_{o=1}^{15}x_o[n]g_{o,c}[n].
$$
其中
$$
\delta_{c,\mathrm{LFE}}
=
\begin{cases}
1, & c\text{ 为目标布局的 LFE},\\
0, & \text{其他声道}.
\end{cases}
$$
没有 LFE 输出的布局不把输入 LFE 混入其他声道。对象完成累加后,再按目标格式要求排列输出声道。
若输出 PCM24,量化关系为
$$
y_{24}[n]
=
\operatorname{trunc}\left(
8388607\,\operatorname{clip}(y[n],-1,1)
\right).
$$
## 14. 公式适用范围
- JOC 矩阵部分描述 dense JOC;Sparse JOC 使用不同的稀疏系数/索引路径。
- 扬声器声像部分描述普通点对象;extent、spread、divergence 等模式需要额外模型。
- 多个 OAMD position block 必须按其时间顺序调度。
- limiter 属于独立后处理,不包含在上述混音公式中。
+159
View File
@@ -0,0 +1,159 @@
# JustOneCacophony native-core notes
[中文](native.md) · [Back to README](../README.en.md)
## 1. Responsibility boundary
`native/` contains only the state-heavy, frequently called DSP and speaker-rendering kernels. High-level EMDF/JOC/OAMD parsing, error reporting, ADM assembly, and the CLI remain in Python.
Python calls a C ABI through the standard-library `ctypes` module. The native core does not use pybind11, Cython, FFTW, MKL, or OpenMP. It is an optional acceleration path and does not expand the set of supported stream variants.
Main files:
```text
native/include/eac3joc_core.h C ABI
native/src/eac3joc_core.cpp JOC/QMF object reconstruction
native/src/speaker_renderer.cpp object-to-speaker rendering
native/src/qmf_tables.h QMF tables
native/src/speaker_layouts.h layout tables
native/src/joc_huffman_tables.h JOC Huffman tables
src/native_renderer.py JOC ctypes bridge
src/speaker_native_renderer.py speaker ctypes bridge
```
## 2. JOC rendering ABI
An opaque renderer owns all cross-frame state. Its main call is:
```c
int ejoc_renderer_process(
ejoc_renderer_handle handle,
const float* bed5_planar, /* [5][1536] */
const float* lfe, /* [1536] or NULL */
uint32_t object_mask,
const uint8_t* n_bands, /* [15] */
const uint8_t* n_dpoints, /* [15] */
const uint8_t* slope_idx, /* [15] */
const uint8_t* offset_ts, /* [15][2] */
const double* dq, /* [15][2][5][23] */
double clipgain,
float phase_new,
float output_scale,
float* output16_planar); /* [16][1536] */
```
Python performs dense-JOC Huffman decoding, differential reconstruction, and dequantization before the call. Sparse JOC is not silently passed to the dense native path.
Thread control is exposed as:
```c
int ejoc_renderer_set_threads(ejoc_renderer_handle handle, uint32_t total_threads);
uint32_t ejoc_renderer_thread_count(ejoc_renderer_handle handle);
```
`total_threads` includes the calling thread. Frames must be submitted sequentially to one renderer instance; the instance may parallelize work across objects and analysis channels.
## 3. Cross-frame state
Each JOC renderer stores:
- analysis FIFO: `double[5][9][64]`;
- L/R/C analysis delay: `float[3][10][64]`;
- Ls/Rs QMF delay: `complex<double>[2][10][64]`;
- Ls/Rs band-0 FIR history: `complex<double>[2][20]`;
- previous matrix interpolation values: `double[15][5][64]`;
- inverse-QMF state: `double[15][640]`;
- LFE delay: `double[1217]`.
This state belongs to the renderer instance. Processing cannot be arbitrarily segmented or reordered without a corresponding state checkpoint.
## 4. FFT, QMF, and precision
The native core contains a fixed 64-point radix-2 complex FFT:
- analysis QMF uses a forward FFT followed by division by 64;
- inverse QMF uses the fixed reorder, rotation, and 640-value active-window state;
- no external FFT library is called.
The JOC path uses:
- float32 core-PCM input;
- double matrices, complex QMF, FFT, FIR, and cross-frame state;
- float32 phase and final gain;
- float32 16-channel object output.
## 5. Speaker-rendering ABI
The same shared library exports object-to-speaker rendering:
```c
uint32_t ejoc_speaker_layout_channel_count(uint32_t speaker_bitfield);
ejoc_speaker_renderer_handle
ejoc_speaker_renderer_create(uint32_t speaker_bitfield);
int ejoc_speaker_renderer_process(
ejoc_speaker_renderer_handle handle,
const float* objects16_interleaved,
uint32_t sample_count,
uint32_t metadata_count,
const uint32_t* metadata_offsets,
const uint32_t* ramp_durations,
const uint16_t* positions_q15,
const uint8_t* region_indices,
const uint8_t* height_enabled,
const double* object_gains,
double* output_interleaved);
```
Input channel 0 is LFE and channels 1–15 are objects. Each metadata entry is an object-state snapshot. `sample_count` must be a multiple of 32; unfinished gain ramps remain in the handle and continue across calls.
The speaker path uses float32 object input, double coordinates/gains/accumulation, and interleaved double output. Quantization to float32 or PCM24 happens when the WAV is written.
Supported layouts:
```text
2.0 3.1 5.1 7.1 5.1.2 5.1.4 7.1.2 7.1.4 9.1.4 9.1.6
```
## 6. Building
The CMake definition is `native/CMakeLists.txt`. Run from the repository root:
```powershell
cmake -S native -B build/cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$PWD/lib"
cmake --build build/cmake --config Release
cmake --install build/cmake --config Release
```
Platform runtime names:
```text
Windows lib/eac3joc_core.dll
Linux lib/libeac3joc_core.so
macOS lib/libeac3joc_core.dylib
```
The MSVC configuration uses the static CRT. Other runtime dependencies depend on the platform and toolchain and should be checked independently before publishing a prebuilt library.
The repository does not include native binaries by default. A prebuilt Release runtime or a locally built runtime can be placed directly under `lib/`.
## 7. Runtime lookup and fallback
Lookup order:
1. explicit `--native-library`;
2. `EAC3JOC_NATIVE_LIBRARY`;
3. the standard platform filename under `lib/`.
`--backend auto` falls back to NumPy when loading fails, and `--backend python` skips native discovery. The current CLI also prints the failure and falls back for `--backend native`; this existing behavior should not be read as successful native execution.
## 8. Implementation boundaries
- The native layer accepts only dense-JOC data already parsed by Python.
- The ABI fixes a 1536-sample JOC frame, at most 15 objects, at most 23 parameter bands, and at most 2 data points.
- The shared library and Python bridge must report the same ABI version.
- Only the ABI and data types are specified across platforms; bit-identical float64 results are not guaranteed.
- Private table headers under `native/src/` serve the native side only. The current repository does not include the scripts that generated those headers.
See the [mathematical notes](math.en.md) for the related formulas.
+159
View File
@@ -0,0 +1,159 @@
# JustOneCacophony 原生核说明
[English](native.en.md) · [返回 README](../README.md)
## 1. 职责边界
`native/` 只承载状态密集、调用频繁的 DSP 与扬声器渲染核。EMDF/JOC/OAMD 高层解析、错误报告、ADM 组装和 CLI 保留在 Python 中。
Python 通过标准库 `ctypes` 调用 C ABI;原生核不使用 pybind11、Cython、FFTW、MKL 或 OpenMP。它是可选加速路径,不扩大项目所支持的码流范围。
主要文件:
```text
native/include/eac3joc_core.h C ABI
native/src/eac3joc_core.cpp JOC/QMF 对象重建
native/src/speaker_renderer.cpp 对象到扬声器渲染
native/src/qmf_tables.h QMF 表
native/src/speaker_layouts.h 布局表
native/src/joc_huffman_tables.h JOC Huffman 表
src/native_renderer.py JOC ctypes 桥
src/speaker_native_renderer.py 扬声器 ctypes 桥
```
## 2. JOC 渲染 ABI
一个 opaque renderer 保存所有跨帧状态。主要调用为:
```c
int ejoc_renderer_process(
ejoc_renderer_handle handle,
const float* bed5_planar, /* [5][1536] */
const float* lfe, /* [1536] or NULL */
uint32_t object_mask,
const uint8_t* n_bands, /* [15] */
const uint8_t* n_dpoints, /* [15] */
const uint8_t* slope_idx, /* [15] */
const uint8_t* offset_ts, /* [15][2] */
const double* dq, /* [15][2][5][23] */
double clipgain,
float phase_new,
float output_scale,
float* output16_planar); /* [16][1536] */
```
Dense JOC 的 Huffman 解码、差分还原和去量化先在 Python 中完成。Sparse JOC 不会被静默送入 dense 原生路径。
线程接口为:
```c
int ejoc_renderer_set_threads(ejoc_renderer_handle handle, uint32_t total_threads);
uint32_t ejoc_renderer_thread_count(ejoc_renderer_handle handle);
```
`total_threads` 包含调用线程。单个 renderer 实例必须顺序提交帧;实例内部可以按对象和 analysis channel 并行。
## 3. 跨帧状态
每个 JOC renderer 独立保存:
- analysis FIFO:`double[5][9][64]`;
- L/R/C analysis delay:`float[3][10][64]`;
- Ls/Rs QMF delay:`complex<double>[2][10][64]`;
- Ls/Rs band-0 FIR history:`complex<double>[2][20]`;
- 矩阵插值 previous:`double[15][5][64]`;
- inverse-QMF state:`double[15][640]`;
- LFE delay:`double[1217]`。
这些状态属于 renderer 实例,不能在无 checkpoint 的情况下任意分段或乱序处理。
## 4. FFT、QMF 与精度
原生核包含固定 64 点 radix-2 complex FFT:
- analysis QMF 使用 forward FFT 后除以 64;
- inverse QMF 使用固定重排、旋转和 640 项有效窗状态;
- 不调用外部 FFT 库。
JOC 路径的数值类型为:
- 核心 PCM 输入:float32;
- 矩阵、复 QMF、FFT、FIR 和跨帧状态:double;
- phase 与最终 gain:float32;
- 16 声道对象输出:float32。
## 5. 扬声器渲染 ABI
同一个共享库还导出对象到扬声器布局的渲染接口:
```c
uint32_t ejoc_speaker_layout_channel_count(uint32_t speaker_bitfield);
ejoc_speaker_renderer_handle
ejoc_speaker_renderer_create(uint32_t speaker_bitfield);
int ejoc_speaker_renderer_process(
ejoc_speaker_renderer_handle handle,
const float* objects16_interleaved,
uint32_t sample_count,
uint32_t metadata_count,
const uint32_t* metadata_offsets,
const uint32_t* ramp_durations,
const uint16_t* positions_q15,
const uint8_t* region_indices,
const uint8_t* height_enabled,
const double* object_gains,
double* output_interleaved);
```
输入声道 0 为 LFE,1–15 为对象。每个 metadata entry 是一份对象状态快照。`sample_count` 必须是 32 的倍数;未完成的增益斜坡保存在 handle 中并跨调用继续。
扬声器路径使用 float32 对象输入、double 坐标/增益/累加与 interleaved double 输出;写 WAV 时才量化为 float32 或 PCM24。
支持的布局为:
```text
2.0 3.1 5.1 7.1 5.1.2 5.1.4 7.1.2 7.1.4 9.1.4 9.1.6
```
## 6. 构建
CMake 定义位于 `native/CMakeLists.txt`。从仓库根目录运行:
```powershell
cmake -S native -B build/cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$PWD/lib"
cmake --build build/cmake --config Release
cmake --install build/cmake --config Release
```
平台运行库文件名:
```text
Windows lib/eac3joc_core.dll
Linux lib/libeac3joc_core.so
macOS lib/libeac3joc_core.dylib
```
MSVC 配置使用静态 CRT。其他运行时依赖由平台和工具链决定,发布预构建库前应对产物独立检查。
仓库默认不附带原生二进制。预构建的 Release 运行库或自行构建的运行库均可直接放入 `lib/`。
## 7. 运行时查找与回退
查找顺序为:
1. 显式 `--native-library`;
2. `EAC3JOC_NATIVE_LIBRARY`;
3. `lib/` 下当前平台的标准文件名。
`--backend auto` 在加载失败时回退到 NumPy;`--backend python` 跳过原生探测。`--backend native` 当前也会打印失败原因后回退,这是现有 CLI 行为,不应理解为原生库已成功使用。
## 8. 实现边界
- 原生层只接收 Python 已解析的 dense JOC 数据。
- ABI 固定了 1536-sample JOC 帧、最多 15 个对象、最多 23 个参数带和最多 2 个数据点。
- 共享库与 Python 桥需要 ABI version 一致。
- 跨平台只约定 ABI 与数据类型,不保证 float64 结果逐位一致。
- `native/src/` 中的私有表头只服务于原生侧;当前仓库不包含重新生成这些头文件的脚本。
相关公式见[数学说明](math.md)。
+519
View File
@@ -0,0 +1,519 @@
"""JustOneCacophony 的 E-AC-3 JOC 命令行入口。"""
import argparse
import hashlib
import json
import math
import os
from pathlib import Path
import platform
import shutil
import subprocess
import sys
import tempfile
import time
PROJECT_DIR = Path(__file__).resolve().parent
SOURCE_DIR = PROJECT_DIR / "src"
if str(SOURCE_DIR) not in sys.path:
sys.path.insert(0, str(SOURCE_DIR))
import numpy as np
import adm_assemble
from adm_validate import validate
from metadata import DirectPayloadIndex, PayloadIndex, write_summary
import oamd_tracks
from renderer import JocRenderer
from native_renderer import NativeBackendUnavailable, NativeJocRenderer
from speaker_backend import create_speaker_renderer
from speaker_layouts import (SPEAKER_LAYOUT_CHOICES, get_speaker_layout,
speaker_layout_display_name)
from speaker_wav import SpeakerPcmSpool, write_speaker_wav
from variant_error import UnsupportedVariantError, write_variant_report
RATE = 48000
FRAME_SAMPLES = 1536
DEFAULT_OUTPUT_DIR = PROJECT_DIR / "output"
def resolve_output(source, requested=None, speaker_layout=None):
"""解析成品路径;未指定时使用项目内的 ``output`` 目录。"""
source = Path(source)
if requested is not None:
target = Path(requested)
elif speaker_layout is not None:
target = DEFAULT_OUTPUT_DIR / f"{source.stem}.{speaker_layout}.wav"
else:
target = DEFAULT_OUTPUT_DIR / (source.stem + ".adm.wav")
return target.expanduser().resolve()
def executable(value, name):
path = shutil.which(value) if value else None
if path is None and value and Path(value).is_file():
path = str(Path(value).resolve())
if path is None:
raise FileNotFoundError(f"找不到 {name}: {value!r}")
return path
def run(command, label):
print(f"[{label}]", flush=True)
result = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
text=True, encoding="utf-8", errors="replace")
if result.returncode:
tail = result.stderr[-4000:]
raise RuntimeError(f"{label} 失败(exit {result.returncode})\n{tail}")
def timed_call(timings, name, function, *args, **kwargs):
started = time.perf_counter()
try:
return function(*args, **kwargs)
finally:
timings[name] = time.perf_counter() - started
def extract_eac3(ffmpeg, source, target):
if source.suffix.lower() in (".eac3", ".ec3"):
return source
run([ffmpeg, "-hide_banner", "-loglevel", "error", "-y", "-i", str(source),
"-map", "0:a:0", "-vn", "-c:a", "copy", "-f", "eac3", str(target)],
"FFmpeg 提取 E-AC-3")
return target
def decode_core(ffmpeg, eac3, target, duration_sec=None):
# 5.1(side) 的 f32le 顺序为 FL FR FC LFE SL SR;JOC 使用其中 0,1,2,4,5。
command = [ffmpeg, "-hide_banner", "-loglevel", "error", "-y", "-i", str(eac3),
"-map", "0:a:0", "-vn"]
if duration_sec is not None:
command.extend(["-t", f"{duration_sec:.9f}"])
command.extend(["-ac", "6", "-ar", str(RATE),
"-c:a", "pcm_f32le", "-f", "f32le", str(target)])
run(command, "FFmpeg 解码核心 5.1 PCM")
return target
def sha256(path):
digest = hashlib.sha256()
with Path(path).open("rb") as fp:
for block in iter(lambda: fp.read(16 << 20), b""):
digest.update(block)
return digest.hexdigest()
def choose_speaker_output_format(requested_format, clip_action, peak, clipped_values,
*, input_func=input, interactive=None):
"""Resolve int24 clipping interactively or through an explicit policy."""
if requested_format != "int24" or clipped_values == 0:
return requested_format
print(
f"[clip] int24 将发生削波:peak={peak:.9g},超出 [-1,1] 的样本值={clipped_values}",
file=sys.stderr, flush=True)
action = clip_action
if action == "ask":
if interactive is None:
interactive = bool(getattr(sys.stdin, "isatty", lambda: False)())
if not interactive:
raise RuntimeError(
"检测到 int24 削波,但当前不是交互终端;请使用 "
"--clip-action continue、--clip-action float32 或 --clip-action abort")
while True:
answer = input_func(
"继续写 int24 并截断 [i] / 改为 float32 [f,默认] / 取消 [a]:"
).strip().lower()
if answer in ("", "f", "float", "float32"):
action = "float32"
break
if answer in ("i", "int", "int24", "c", "continue"):
action = "continue"
break
if answer in ("a", "abort", "q", "quit", "n", "no"):
action = "abort"
break
print("请输入 i、f 或 a。", file=sys.stderr, flush=True)
if action == "continue":
print("[clip] 将继续写 int24,超范围值会截断到 [-1,1]。", flush=True)
return "int24"
if action == "float32":
print("[clip] 已切换为 float32 WAV,不执行截断。", flush=True)
return "float32"
if action == "abort":
raise RuntimeError("用户因 int24 削波取消输出")
raise ValueError(f"未知 clip action: {action}")
def resolve_metadata(args, eac3, temp_dir):
if args.metadata_dir:
directory = Path(args.metadata_dir).resolve()
return PayloadIndex(directory), "sidecar", directory
if args.metadata_backend == "sidecar":
raise ValueError("metadata-backend=sidecar 时必须提供 --metadata-dir")
cache_dir = (args.metadata_cache.expanduser().resolve()
if args.metadata_cache else None)
max_frames = (math.ceil(args.duration * RATE / FRAME_SAMPLES)
if args.duration is not None else None)
index = DirectPayloadIndex.from_eac3(
eac3, max_frames=max_frames, cache_dir=cache_dir)
return index, "python-emdf-memory", cache_dir
def variant_call(output, source, function, *args, **kwargs):
"""执行一个阶段;遇到未知变体时在目标文件旁写结构化报告。"""
try:
return function(*args, **kwargs)
except UnsupportedVariantError as exc:
report_path = Path(str(output) + ".variant-error.json")
write_variant_report(report_path, exc, input_path=source, output_path=output)
print(f"[VARIANT] {exc}", file=sys.stderr, flush=True)
print(f"[VARIANT] 维修报告: {report_path}", file=sys.stderr, flush=True)
raise
def create_renderer(backend, gain, native_library=None, native_threads=None):
"""选择整帧 DSP 后端;auto 优先使用 lib 中当前平台的原生构建。"""
if backend in ("auto", "native"):
try:
decoder = NativeJocRenderer(
output_scale=gain, library_path=native_library, threads=native_threads)
info = {
"name": "native",
"library": str(decoder.library_path),
"build": decoder.build_info,
"threads": decoder.threads,
}
print(f"[backend] native: {info['build']} threads={info['threads']} "
f"({info['library']})", flush=True)
return decoder, info
except (NativeBackendUnavailable, OSError) as exc:
print(f"[backend] native unavailable, falling back to Python: {exc}", flush=True)
decoder = JocRenderer(output_scale=gain)
info = {"name": "python", "library": None, "build": None, "threads": None}
print("[backend] python/numpy", flush=True)
return decoder, info
def render(index, bed_path, frame_count, raw_path, gain, progress_every,
backend="auto", native_library=None, native_threads=None, frame_sink=None,
speaker_renderer=None, speaker_sink=None, speaker_metadata_offset=1473):
values = np.memmap(bed_path, dtype=np.float32, mode="r")
frame_width = FRAME_SAMPLES * 6
if values.size % frame_width:
raise ValueError(f"FFmpeg PCM 长度不是 1536×6 的整数倍: {values.size}")
bed = values.reshape(-1, FRAME_SAMPLES, 6)
if len(bed) < frame_count:
raise ValueError(f"PCM 只有 {len(bed)} 帧,元数据需要 {frame_count} 帧")
output = (np.memmap(raw_path, dtype=np.float32, mode="w+",
shape=(frame_count, FRAME_SAMPLES, 16))
if raw_path is not None else None)
decoder, backend_info = create_renderer(backend, gain, native_library, native_threads)
started = time.perf_counter()
dsp_seconds = 0.0
adm_stream_seconds = 0.0
raw_write_seconds = 0.0
speaker_render_seconds = 0.0
speaker_write_seconds = 0.0
try:
for frame_number, row in enumerate(index.rows[:frame_count]):
bed6 = np.asarray(bed[frame_number], dtype=np.float32)
subs = index.subpayloads(row)
stage = time.perf_counter()
pcm16, _ = decoder.render_subpayloads(
subs, bed6[:, [0, 1, 2, 4, 5]].T, bed6[:, 3])
dsp_seconds += time.perf_counter() - stage
if output is not None:
stage = time.perf_counter()
output[frame_number] = pcm16.T
raw_write_seconds += time.perf_counter() - stage
if frame_sink is not None:
stage = time.perf_counter()
frame_sink.write_frame(pcm16)
adm_stream_seconds += time.perf_counter() - stage
if speaker_renderer is not None:
stage = time.perf_counter()
speaker_pcm = speaker_renderer.render_frame(
pcm16.T, subs.get(11), speaker_metadata_offset)
speaker_render_seconds += time.perf_counter() - stage
stage = time.perf_counter()
speaker_sink.write_frame(speaker_pcm)
speaker_write_seconds += time.perf_counter() - stage
done = frame_number + 1
if done % progress_every == 0 or done == frame_count:
elapsed = time.perf_counter() - started
speed = done / max(elapsed, 1e-9)
eta = (frame_count - done) / max(speed, 1e-9)
print(f"[JOC:{backend_info['name']}] {done}/{frame_count} "
f"{speed:.1f} frame/s ETA {eta:.1f}s", flush=True)
if output is not None:
output.flush()
elapsed = time.perf_counter() - started
finally:
close = getattr(decoder, "close", None)
if close is not None:
close()
close = getattr(speaker_renderer, "close", None)
if close is not None:
close()
breakdown = {
"pipeline_wall_seconds": elapsed,
"dsp_and_joc_parse_seconds": dsp_seconds,
"adm_stream_write_seconds": adm_stream_seconds,
"raw_float_write_seconds": raw_write_seconds,
"speaker_render_seconds": speaker_render_seconds,
"speaker_spool_write_seconds": speaker_write_seconds,
}
return dsp_seconds, backend_info, breakdown
def build_parser():
parser = argparse.ArgumentParser(
description="JustOneCacophony (JOC):E-AC-3 JOC → 25ch ADM BWF 或扬声器 WAV")
parser.add_argument("input", type=Path, help="输入 .m4a/.eac3/.ec3")
parser.add_argument("-o", "--output", type=Path, help="输出文件;默认按模式和布局命名")
parser.add_argument("--speaker-output", type=Path,
help="扬声器 WAV 路径;仅与 --speaker-layout 一起使用")
parser.add_argument("--speaker-layout", choices=SPEAKER_LAYOUT_CHOICES,
help="直接扬声器渲染布局,例如 2.0、5.1、7.1.2")
parser.add_argument("--speaker-format", choices=("float32", "int24"), default="float32",
help="扬声器 WAV 格式,默认 float32")
parser.add_argument("--clip-action", choices=("ask", "continue", "float32", "abort"),
default="ask",
help="int24 削波处理:交互询问、继续截断、改 float32 或中止")
parser.add_argument("--speaker-metadata-offset", type=int, default=1473,
help="扬声器渲染 metadata 相对帧偏移,默认 1473 samples")
parser.add_argument("--gain-db", type=float, default=0.0,
help="成品增益 dB,默认 0(float32 系数 1.0)")
parser.add_argument("--duration", type=float, help="只处理开头指定秒数")
parser.add_argument("--object-delay-samples", type=int, default=640,
help="可选的对象 PCM/OAMD 时间补偿,默认 640 samples")
parser.add_argument("--trajectory-mode", choices=("compact", "dense64"), default="compact",
help="对象轨迹表示;compact 用长线性插值压缩 AXML,dense64 保留逐 64-sample 块")
parser.add_argument("--ffmpeg", default=os.environ.get("FFMPEG", "ffmpeg"))
parser.add_argument("--backend", choices=("auto", "native", "python"), default="auto",
help="DSP 后端;auto 优先 C++,不可用时回退 Python")
parser.add_argument("--native-library", type=Path,
help="显式指定原生库;默认从单层 lib 目录选择当前平台文件")
parser.add_argument("--native-threads", type=int,
help="原生 DSP 总线程数;默认在 4 核以上使用 2,可用环境变量 EAC3JOC_NATIVE_THREADS 覆盖")
metadata_source = parser.add_mutually_exclusive_group()
metadata_source.add_argument("--metadata-dir", type=Path,
help="含 frames.csv 和 emdf/ 或 payloads/ 的元数据 sidecar")
metadata_source.add_argument("--metadata-cache", type=Path,
help="把直接 EMDF 扫描或兼容桥结果持久保存到此目录")
parser.add_argument("--metadata-backend", choices=("auto", "emdf", "sidecar"),
default="auto", help="直接扫描连续 EMDF,或读取现有 sidecar")
parser.add_argument("--print-metadata", choices=("none", "summary", "frames"), default="none",
help="诊断元数据输出;默认 none,避免转换前重复完整解析")
parser.add_argument("--metadata-json", type=Path, help="元数据汇总 JSON 路径")
parser.add_argument("--metadata-only", action="store_true", help="解析/打印元数据后退出")
parser.add_argument("--keep-raw", action="store_true", help="额外保留 16ch f32le 对象中间文件")
parser.add_argument("--skip-sha256", action="store_true",
help="跳过最终文件 SHA-256 全量复扫以缩短大文件处理时间")
parser.add_argument("--progress-every", type=int, default=500)
return parser
def main(argv=None):
# Windows 控制台的活动代码页未必能表示日文文件名;保留信息并避免
# UnicodeEncodeError 中断长任务。支持 UTF-8 的终端仍会原样显示。
for stream in (sys.stdout, sys.stderr):
if hasattr(stream, "reconfigure"):
stream.reconfigure(encoding="utf-8", errors="backslashreplace")
args = build_parser().parse_args(argv)
source = args.input.expanduser().resolve()
if not source.is_file():
raise FileNotFoundError(source)
speaker_mode = args.speaker_layout is not None
if args.speaker_output is not None and not speaker_mode:
raise ValueError("--speaker-output 必须与 --speaker-layout 一起使用")
if args.output is not None and args.speaker_output is not None:
raise ValueError("-o/--output 与 --speaker-output 不能同时使用")
if args.speaker_metadata_offset < 0:
raise ValueError("speaker-metadata-offset 不能为负数")
requested_output = (args.speaker_output if args.speaker_output is not None
else args.output)
output = resolve_output(
source, requested_output, args.speaker_layout if speaker_mode else None)
output.parent.mkdir(parents=True, exist_ok=True)
if args.duration is not None and args.duration <= 0:
raise ValueError("duration 必须大于 0")
if args.object_delay_samples < 0:
raise ValueError("object-delay-samples 不能为负数")
gain = np.float32(10.0 ** (args.gain_db / 20.0))
if not np.isfinite(gain):
raise ValueError("gain-db 超出 float32 范围")
ffmpeg = executable(args.ffmpeg, "FFmpeg")
total_started = time.perf_counter()
timings = {}
with tempfile.TemporaryDirectory(prefix="eac3joc-", dir=output.parent) as temporary:
temp_dir = Path(temporary)
eac3 = timed_call(timings, "extract_eac3", extract_eac3,
ffmpeg, source, temp_dir / "input.eac3")
index, metadata_backend, metadata_cache_dir = timed_call(
timings, "resolve_metadata", variant_call,
output, source, resolve_metadata, args, eac3, temp_dir)
timings["load_metadata_index"] = 0.0
frame_count = len(index)
if args.duration is not None:
frame_count = min(frame_count, math.ceil(args.duration * RATE / FRAME_SAMPLES))
duration_sec = frame_count * FRAME_SAMPLES / RATE
need_metadata_summary = (
args.metadata_only or args.metadata_json is not None or args.print_metadata != "none")
if need_metadata_summary:
metadata_json = (args.metadata_json or Path(str(output) + ".metadata.json")).resolve()
summary = timed_call(
timings, "metadata_summary", variant_call,
output, source, write_summary, index, metadata_json, limit=frame_count,
print_frames=args.print_metadata == "frames")
if args.print_metadata == "summary":
print("[metadata] " + json.dumps(summary, ensure_ascii=False, separators=(",", ":")))
print(f"[metadata] backend={metadata_backend} frames={frame_count} -> {metadata_json}")
else:
metadata_json = None
timings["metadata_summary"] = 0.0
print(f"[metadata] backend={metadata_backend} frames={frame_count} summary=skipped")
if args.metadata_only:
return 0
bed_path = timed_call(
timings, "decode_core", decode_core,
ffmpeg, eac3, temp_dir / "core51_f32le.raw", duration_sec)
raw_path = (output.with_name(output.name + ".objects16.f32le")
if args.keep_raw else None)
master = None
speaker_backend_info = None
speaker_wav_info = None
speaker_clip_info = None
speaker_actual_format = None
if speaker_mode:
layout = get_speaker_layout(args.speaker_layout)
speaker_name = speaker_layout_display_name(layout)
speaker_decoder, speaker_backend_info = create_speaker_renderer(
layout, backend=args.backend, native_library=args.native_library)
fallback = speaker_backend_info.get("fallback_reason")
if fallback:
print(f"[speaker] native unavailable, falling back to Python: {fallback}",
flush=True)
print(f"[speaker] layout={speaker_name} backend={speaker_backend_info['name']} "
f"channels={layout.channel_count}", flush=True)
spool = SpeakerPcmSpool(
temp_dir / "speaker_interleaved_f32.raw",
frame_count * FRAME_SAMPLES, layout.channel_count)
try:
render_seconds, renderer_backend, render_breakdown = timed_call(
timings, "render_and_stream", variant_call,
output, source, render, index, bed_path, frame_count, raw_path, gain,
max(1, args.progress_every), args.backend, args.native_library,
args.native_threads, None, speaker_decoder, spool,
args.speaker_metadata_offset)
spool.finalize()
speaker_actual_format = choose_speaker_output_format(
args.speaker_format, args.clip_action, spool.peak,
spool.clipped_values)
speaker_wav_info = timed_call(
timings, "write_speaker_wav", write_speaker_wav,
output, spool.values, speaker_actual_format, rate=RATE)
speaker_clip_info = {
"peak": spool.peak,
"over_unity_values": spool.clipped_values,
"requested_format": args.speaker_format,
"actual_format": speaker_actual_format,
"clip_action": args.clip_action,
}
finally:
spool.close()
timings["build_adm_tracks"] = 0.0
timings["finalize_adm"] = 0.0
timings["validate_adm"] = 0.0
info = (f"speaker layout={speaker_name}, format={speaker_actual_format}, "
f"peak={speaker_clip_info['peak']:.9g}")
else:
master = adm_assemble.StreamingMaster(output, duration_sec, rate=RATE)
try:
render_seconds, renderer_backend, render_breakdown = timed_call(
timings, "render_and_stream", variant_call,
output, source, render, index, bed_path, frame_count, raw_path, gain,
max(1, args.progress_every), args.backend, args.native_library,
args.native_threads, master)
tracks = timed_call(
timings, "build_adm_tracks", variant_call,
output, source, oamd_tracks.build_adm_tracks,
index, index.rows[:frame_count], rate=RATE, frame_samples=FRAME_SAMPLES,
object_delay_samples=args.object_delay_samples,
trajectory_mode=args.trajectory_mode)
timed_call(timings, "finalize_adm", master.finalize, tracks)
except Exception:
master.abort()
raise
errors, info = timed_call(timings, "validate_adm", validate, str(output))
if errors:
raise RuntimeError("ADM 校验失败: " + "; ".join(errors))
# Windows 不允许删除仍被 NumPy memmap 持有的临时 core/raw;显式回收闭包。
import gc
gc.collect()
if args.skip_sha256:
output_sha = None
timings["sha256"] = 0.0
else:
output_sha = timed_call(timings, "sha256", sha256, output)
total_seconds = time.perf_counter() - total_started
report = {
"input": str(source),
"output": str(output),
"mode": "speaker" if speaker_mode else "adm",
"metadata": str(metadata_json) if metadata_json is not None else None,
"metadata_backend": metadata_backend,
"metadata_cache": str(metadata_cache_dir) if metadata_cache_dir is not None else None,
"frames": frame_count,
"duration_sec": duration_sec,
"gain_db": args.gain_db,
"gain_float32": float(gain),
"object_delay_samples": None if speaker_mode else args.object_delay_samples,
"trajectory_mode": None if speaker_mode else args.trajectory_mode,
"render_seconds": render_seconds,
"render_breakdown": render_breakdown,
"renderer_backend": renderer_backend,
"speaker_renderer_backend": speaker_backend_info,
"speaker_layout": args.speaker_layout if speaker_mode else None,
"speaker_metadata_offset": args.speaker_metadata_offset if speaker_mode else None,
"speaker_clip": speaker_clip_info,
"speaker_wav": speaker_wav_info,
"streaming_adm": not speaker_mode,
"kept_raw": str(raw_path) if raw_path is not None else None,
"timings": timings,
"total_seconds": total_seconds,
"adm_validation": None if speaker_mode else info,
"adm_metadata": getattr(master, "metadata_info", None) if master is not None else None,
"sha256": output_sha,
"python": platform.python_version(),
"numpy": np.__version__,
}
report_path = Path(str(output) + ".report.json")
report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"[PASS] {output}")
if report["sha256"] is None:
print(f"[PASS] {info}; SHA-256 skipped")
else:
print(f"[PASS] {info}; SHA-256={report['sha256']}")
if speaker_mode:
print(f"[time] JOC-DSP={render_seconds:.2f}s ({renderer_backend['name']}) "
f"speaker={render_breakdown['speaker_render_seconds']:.2f}s "
f"pipeline={render_breakdown['pipeline_wall_seconds']:.2f}s "
f"total={report['total_seconds']:.2f}s")
else:
print(f"[time] DSP={render_seconds:.2f}s ({renderer_backend['name']}) "
f"render+ADM-stream={render_breakdown['pipeline_wall_seconds']:.2f}s "
f"total={report['total_seconds']:.2f}s")
print(f"[report] {report_path}")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except KeyboardInterrupt:
raise SystemExit(130)
+72
View File
@@ -0,0 +1,72 @@
cmake_minimum_required(VERSION 3.20)
project(eac3joc_core VERSION 1.0.0 LANGUAGES CXX)
include(GNUInstallDirs)
find_package(Threads REQUIRED)
add_library(eac3joc_core SHARED
src/eac3joc_core.cpp
src/speaker_renderer.cpp
src/joc_huffman_tables.h
src/qmf_tables.h
src/speaker_layouts.h
)
target_compile_features(eac3joc_core PRIVATE cxx_std_20)
target_include_directories(eac3joc_core PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/include"
)
target_link_libraries(eac3joc_core PRIVATE Threads::Threads)
set_target_properties(eac3joc_core PROPERTIES
OUTPUT_NAME "eac3joc_core"
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
POSITION_INDEPENDENT_CODE YES
)
if(MSVC)
set_property(TARGET eac3joc_core PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
target_compile_options(eac3joc_core PRIVATE
/W4 /permissive- /Zc:__cplusplus /utf-8 /EHsc /GR- /fp:precise
$<$<CONFIG:Release>:/O2>
$<$<CONFIG:Release>:/Oi>
$<$<CONFIG:Release>:/GL>
)
target_link_options(eac3joc_core PRIVATE
$<$<CONFIG:Release>:/LTCG>
/INCREMENTAL:NO /OPT:REF /OPT:ICF
)
else()
target_compile_options(eac3joc_core PRIVATE
-Wall -Wextra -Wpedantic -fno-fast-math
$<$<CONFIG:Release>:-O3>
)
endif()
# Install directly into the chosen prefix. Recommended invocation from the
# repository root:
# cmake -S native -B build/cmake -DCMAKE_BUILD_TYPE=Release \
# -DCMAKE_INSTALL_PREFIX=<repo>/lib
# cmake --build build/cmake --config Release
# cmake --install build/cmake --config Release
#
# Result names are supplied by the platform toolchain:
# Windows: eac3joc_core.dll (+ eac3joc_core.lib import library)
# Linux: libeac3joc_core.so
# macOS: libeac3joc_core.dylib
install(TARGETS eac3joc_core
RUNTIME DESTINATION .
LIBRARY DESTINATION .
ARCHIVE DESTINATION .
)
if(MSVC)
install(FILES "$<TARGET_PDB_FILE:eac3joc_core>"
DESTINATION .
OPTIONAL
)
endif()
+119
View File
@@ -0,0 +1,119 @@
#pragma once
#include <stdint.h>
#if defined(_WIN32)
#if defined(EJOC_BUILD_DLL)
#define EJOC_API __declspec(dllexport)
#else
#define EJOC_API __declspec(dllimport)
#endif
#define EJOC_CALL __cdecl
#else
#define EJOC_API __attribute__((visibility("default")))
#define EJOC_CALL
#endif
#ifdef __cplusplus
extern "C" {
#endif
enum {
EJOC_ABI_VERSION = 1,
EJOC_FRAME_SAMPLES = 1536,
EJOC_TIMESLOTS = 24,
EJOC_SUBBANDS = 64,
EJOC_CORE_CHANNELS = 5,
EJOC_OUTPUT_CHANNELS = 16,
EJOC_MAX_OBJECTS = 15,
EJOC_MAX_DPOINTS = 2,
EJOC_MAX_PARAMETER_BANDS = 23,
EJOC_SPEAKER_BLOCK_SAMPLES = 32,
EJOC_SPEAKER_COORDINATES = 3
};
typedef void* ejoc_renderer_handle;
typedef void* ejoc_speaker_renderer_handle;
/*
Fixed array layouts used by ejoc_renderer_process():
bed5_planar [5][1536]
lfe [1536] or NULL
n_bands [15]
n_dpoints [15]
slope_idx [15]
offset_ts [15][2]
dq [15][2][5][23]
output16 [16][1536]
Only objects selected by object_mask are read from the descriptor arrays.
Sparse JOC must be rejected by the caller; this ABI accepts already dequantized
dense matrix coefficients.
*/
EJOC_API uint32_t EJOC_CALL ejoc_abi_version(void);
EJOC_API const char* EJOC_CALL ejoc_build_info(void);
EJOC_API ejoc_renderer_handle EJOC_CALL ejoc_renderer_create(void);
EJOC_API void EJOC_CALL ejoc_renderer_destroy(ejoc_renderer_handle handle);
EJOC_API int EJOC_CALL ejoc_renderer_reset(ejoc_renderer_handle handle);
EJOC_API int EJOC_CALL ejoc_renderer_set_threads(ejoc_renderer_handle handle, uint32_t total_threads);
EJOC_API uint32_t EJOC_CALL ejoc_renderer_thread_count(ejoc_renderer_handle handle);
EJOC_API const char* EJOC_CALL ejoc_renderer_last_error(ejoc_renderer_handle handle);
EJOC_API int EJOC_CALL ejoc_renderer_process(
ejoc_renderer_handle handle,
const float* bed5_planar,
const float* lfe,
uint32_t object_mask,
const uint8_t* n_bands,
const uint8_t* n_dpoints,
const uint8_t* slope_idx,
const uint8_t* offset_ts,
const double* dq,
double clipgain,
float phase_new,
float output_scale,
float* output16_planar);
/*
High-precision object-to-speaker renderer.
The renderer consumes interleaved float32 input PCM arranged as:
objects16_interleaved [sample_count][16]
where channel 0 is LFE and channels 1..15 are point objects. All spatial
calculations, gain ramps, and accumulation use double. Output is interleaved:
output_interleaved [sample_count][layout_channel_count]
Each metadata entry is a complete object-state snapshot:
metadata_offsets [metadata_count], relative to this process call
ramp_durations [metadata_count], in samples
positions_q15 [metadata_count][15][3]
region_indices [metadata_count][15] or NULL (all region 0)
height_enabled [metadata_count][15] or NULL (all enabled)
object_gains [metadata_count][15] or NULL (all 1.0)
metadata_offsets must be nondecreasing and <= sample_count. sample_count must
be a multiple of EJOC_SPEAKER_BLOCK_SAMPLES. State and unfinished ramps are
preserved across calls.
*/
EJOC_API uint32_t EJOC_CALL ejoc_speaker_layout_channel_count(uint32_t speaker_bitfield);
EJOC_API ejoc_speaker_renderer_handle EJOC_CALL ejoc_speaker_renderer_create(uint32_t speaker_bitfield);
EJOC_API void EJOC_CALL ejoc_speaker_renderer_destroy(ejoc_speaker_renderer_handle handle);
EJOC_API int EJOC_CALL ejoc_speaker_renderer_reset(ejoc_speaker_renderer_handle handle);
EJOC_API const char* EJOC_CALL ejoc_speaker_renderer_last_error(ejoc_speaker_renderer_handle handle);
EJOC_API int EJOC_CALL ejoc_speaker_renderer_process(
ejoc_speaker_renderer_handle handle,
const float* objects16_interleaved,
uint32_t sample_count,
uint32_t metadata_count,
const uint32_t* metadata_offsets,
const uint32_t* ramp_durations,
const uint16_t* positions_q15,
const uint8_t* region_indices,
const uint8_t* height_enabled,
const double* object_gains,
double* output_interleaved);
#ifdef __cplusplus
}
#endif
+735
View File
@@ -0,0 +1,735 @@
#define EJOC_BUILD_DLL
#include "eac3joc_core.h"
#include "qmf_tables.h"
#include <algorithm>
#include <atomic>
#include <barrier>
#include <cmath>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <memory>
#include <new>
#include <thread>
#include <vector>
namespace ejoc {
struct Complex {
double re;
double im;
};
inline Complex mul(const Complex a, const Complex b) noexcept {
return {a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re};
}
inline double clamp_unit(double value) noexcept {
return value < -1.0 ? -1.0 : (value > 1.0 ? 1.0 : value);
}
constexpr double kPi = 3.141592653589793238462643383279502884;
constexpr int kLfeDelay = 1217;
constexpr int kMaxBands = EJOC_MAX_PARAMETER_BANDS;
const uint8_t* parameter_band_map(const int bands) noexcept {
using namespace tables;
switch (bands) {
case 1: return kPbMap1;
case 3: return kPbMap3;
case 5: return kPbMap5;
case 7: return kPbMap7;
case 9: return kPbMap9;
case 12: return kPbMap12;
case 15: return kPbMap15;
case 23: return kPbMap23;
default: return nullptr;
}
}
class Renderer final {
public:
Renderer() noexcept {
initialize_tables();
reset();
}
~Renderer() noexcept {
stop_workers();
}
int reset() noexcept {
std::memset(analysis_fifo_, 0, sizeof(analysis_fifo_));
std::memset(analysis_delay_, 0, sizeof(analysis_delay_));
std::memset(surround_delay_, 0, sizeof(surround_delay_));
std::memset(surround_history_, 0, sizeof(surround_history_));
std::memset(lfe_delay_, 0, sizeof(lfe_delay_));
std::memset(matrix_previous_, 0, sizeof(matrix_previous_));
std::memset(synthesis_state_, 0, sizeof(synthesis_state_));
std::memset(x_, 0, sizeof(x_));
std::memset(z_, 0, sizeof(z_));
analysis_phase_ = 0.0625f;
error_[0] = '\0';
return 0;
}
int set_threads(uint32_t total_threads) noexcept {
if (total_threads < 1) {
total_threads = 1;
}
if (total_threads > EJOC_MAX_OBJECTS) {
total_threads = EJOC_MAX_OBJECTS;
}
stop_workers();
job_count_ = 0;
next_job_.store(0, std::memory_order_relaxed);
if (total_threads == 1) {
return 0;
}
try {
stop_.store(false, std::memory_order_relaxed);
pool_ready_.store(false, std::memory_order_relaxed);
work_barrier_ = std::make_unique<std::barrier<>>(static_cast<std::ptrdiff_t>(total_threads));
workers_.reserve(total_threads - 1);
for (uint32_t index = 1; index < total_threads; ++index) {
workers_.emplace_back([this]() noexcept { worker_loop(); });
}
pool_ready_.store(true, std::memory_order_release);
// Startup rendezvous: ensure every worker has entered the two-phase
// barrier loop before set_threads returns, so an immediate destroy
// cannot race a worker that exits before reaching the barrier.
work_barrier_->arrive_and_wait();
work_barrier_->arrive_and_wait();
} catch (...) {
stop_.store(true, std::memory_order_release);
pool_ready_.store(true, std::memory_order_release);
for (std::thread& worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
workers_.clear();
work_barrier_.reset();
stop_.store(false, std::memory_order_relaxed);
return fail("failed to create native worker threads");
}
return 0;
}
uint32_t thread_count() const noexcept {
return static_cast<uint32_t>(workers_.size() + 1);
}
const char* error() const noexcept {
return error_[0] ? error_ : "";
}
int process(
const float* bed5,
const float* lfe,
const uint32_t object_mask,
const uint8_t* n_bands,
const uint8_t* n_dpoints,
const uint8_t* slope_idx,
const uint8_t* offset_ts,
const double* dq,
const double clipgain,
const float phase_new,
const float output_scale,
float* output16) noexcept {
error_[0] = '\0';
if (!bed5 || !n_bands || !n_dpoints || !slope_idx || !offset_ts || !dq || !output16) {
return fail("null pointer passed to ejoc_renderer_process");
}
if (object_mask & ~((1u << EJOC_MAX_OBJECTS) - 1u)) {
return fail("object_mask contains an object index above 14");
}
if (!std::isfinite(clipgain) || !std::isfinite(phase_new) || !std::isfinite(output_scale)) {
return fail("clipgain, phase_new, and output_scale must be finite");
}
for (int object = 0; object < EJOC_MAX_OBJECTS; ++object) {
if ((object_mask & (1u << object)) == 0) {
continue;
}
if (!parameter_band_map(n_bands[object])) {
return fail("unsupported parameter-band count");
}
if (n_dpoints[object] < 1 || n_dpoints[object] > 2) {
return fail("n_dpoints must be 1 or 2");
}
if (slope_idx[object] > 1) {
return fail("slope_idx must be 0 or 1");
}
}
std::memset(output16, 0, sizeof(float) * EJOC_OUTPUT_CHANNELS * EJOC_FRAME_SAMPLES);
analysis(bed5, phase_new);
render_lfe(lfe, output_scale, output16);
process_objects(object_mask, n_bands, n_dpoints, slope_idx, offset_ts,
dq, clipgain, output_scale, output16);
return 0;
}
private:
void initialize_tables() noexcept {
for (int i = 0; i < 64; ++i) {
int value = i;
int reversed = 0;
for (int bit = 0; bit < 6; ++bit) {
reversed = (reversed << 1) | (value & 1);
value >>= 1;
}
bit_reverse_[i] = static_cast<uint8_t>(reversed);
const double theta = kPi * static_cast<double>(i) / 128.0;
rotation_sin_[i] = 0.5 * std::sin(theta);
rotation_cos_[i] = 0.5 * std::cos(theta);
}
for (int i = 0; i < 32; ++i) {
const double angle = -2.0 * kPi * static_cast<double>(i) / 64.0;
fft_twiddle_[i] = {std::cos(angle), std::sin(angle)};
}
}
int fail(const char* message) noexcept {
std::snprintf(error_, sizeof(error_), "%s", message);
return -1;
}
void fft64(Complex* values) const noexcept {
for (int i = 0; i < 64; ++i) {
const int j = bit_reverse_[i];
if (j > i) {
const Complex temp = values[i];
values[i] = values[j];
values[j] = temp;
}
}
for (int length = 2; length <= 64; length <<= 1) {
const int half = length >> 1;
const int twiddle_step = 64 / length;
for (int base = 0; base < 64; base += length) {
for (int j = 0; j < half; ++j) {
const Complex even = values[base + j];
const Complex odd = mul(values[base + j + half], fft_twiddle_[j * twiddle_step]);
values[base + j] = {even.re + odd.re, even.im + odd.im};
values[base + j + half] = {even.re - odd.re, even.im - odd.im};
}
}
}
}
void analysis_slot(const int channel, const float* ring, const int timeslot) noexcept {
double v36[64];
double v40[64];
Complex frequency[64];
for (int sample = 0; sample < 64; ++sample) {
v36[sample] =
analysis_fifo_[channel][0][sample] * tables::kAnalysisWindow[8 * 64 + sample] +
analysis_fifo_[channel][2][sample] * tables::kAnalysisWindow[6 * 64 + sample] +
analysis_fifo_[channel][4][sample] * tables::kAnalysisWindow[4 * 64 + sample] +
analysis_fifo_[channel][6][sample] * tables::kAnalysisWindow[2 * 64 + sample] +
analysis_fifo_[channel][8][sample] * tables::kAnalysisWindow[0 * 64 + sample];
v40[sample] =
analysis_fifo_[channel][1][sample] * tables::kAnalysisWindow[7 * 64 + sample] +
analysis_fifo_[channel][3][sample] * tables::kAnalysisWindow[5 * 64 + sample] +
analysis_fifo_[channel][5][sample] * tables::kAnalysisWindow[3 * 64 + sample] +
analysis_fifo_[channel][7][sample] * tables::kAnalysisWindow[1 * 64 + sample] +
static_cast<double>(ring[sample]) * tables::kAnalysisWindow[9 * 64 + sample];
}
for (int k = 0; k < 64; ++k) {
const int source = 63 - k;
const double re = v40[source];
const double im = v36[source];
const double a = rotation_sin_[k];
const double b = rotation_cos_[k];
frequency[k] = {im * a - re * b, im * b + re * a};
}
fft64(frequency);
constexpr double scale = 1.0 / 64.0;
for (int k = 0; k < 32; ++k) {
x_[channel][2 * k][timeslot] = {frequency[k].re * scale, -frequency[k].im * scale};
x_[channel][2 * k + 1][timeslot] = {
frequency[63 - k].re * scale,
frequency[63 - k].im * scale};
}
for (int history = 8; history > 0; --history) {
std::memcpy(analysis_fifo_[channel][history], analysis_fifo_[channel][history - 1],
sizeof(analysis_fifo_[channel][history]));
}
for (int sample = 0; sample < 64; ++sample) {
analysis_fifo_[channel][0][sample] = static_cast<double>(ring[sample]);
}
}
void surround_post() noexcept {
static constexpr double kDcA[21] = {
-.0006242550443857908, -.0019234686624258757, -.0042654648423194885,
-.008168308064341545, -.014327201060950756, -.023759860545396805,
-.03757232800126076, -.05577569454908371, -.07568276673555374,
-.09172472357749939, -.5979374051094055, -.09172472357749939,
-.07568276673555374, -.05577569454908371, -.03757232800126076,
-.023759860545396805, -.014327201060950756, -.008168308064341545,
-.0042654648423194885, -.0019234686624258757, -.0006242550443857908,
};
static constexpr double kDcB[21] = {
.0013996040215715766, .003839150769636035, .007512642536312342,
.012419373728334904, .018367428332567215, .0249701626598835,
.03167900815606117, .03785000368952751, .04283412545919418,
.04607561603188515, .047200120985507965, .04607561603188515,
.04283412545919418, .03785000368952751, .03167900815606117,
.0249701626598835, .018367428332567215, .012419373728334904,
.007512642536312342, .003839150769636035, .0013996040215715766,
};
for (int surround = 0; surround < 2; ++surround) {
const int channel = surround + 3;
for (int group = 0; group < 24; group += 4) {
Complex current[4][64];
Complex dc_buffer[24];
for (int slot = 0; slot < 4; ++slot) {
for (int band = 0; band < 64; ++band) {
current[slot][band] = x_[channel][band][group + slot];
const Complex delayed = surround_delay_[surround][slot][band];
x_[channel][band][group + slot] = {delayed.im, -delayed.re};
}
}
for (int i = 0; i < 20; ++i) {
dc_buffer[i] = surround_history_[surround][i];
}
for (int i = 0; i < 4; ++i) {
dc_buffer[20 + i] = current[i][0];
}
for (int slot = 0; slot < 4; ++slot) {
Complex sum{0.0, 0.0};
for (int tap = 0; tap < 21; ++tap) {
const Complex sample = dc_buffer[slot + tap];
const double cr = kDcB[tap];
const double ci = kDcA[tap];
sum.re += sample.re * cr - sample.im * ci;
sum.im += sample.re * ci + sample.im * cr;
}
x_[channel][0][group + slot] = {2.0 * sum.re, 2.0 * sum.im};
}
for (int i = 0; i < 20; ++i) {
surround_history_[surround][i] = dc_buffer[i + 4];
}
std::memmove(&surround_delay_[surround][0][0],
&surround_delay_[surround][4][0],
sizeof(Complex) * 6 * 64);
for (int slot = 0; slot < 4; ++slot) {
std::memcpy(surround_delay_[surround][6 + slot], current[slot],
sizeof(Complex) * 64);
}
}
}
}
void analysis_channel(const int channel) noexcept {
const float* bed5 = job_bed5_;
const float phase_old = job_phase_old_;
const float phase_new = job_phase_new_;
const bool ramp_phase = phase_old != phase_new;
const float phase_step = static_cast<float>((phase_new - phase_old) / 256.0f);
float scaled[64];
float delayed[64];
for (int timeslot = 0; timeslot < 24; ++timeslot) {
for (int sample = 0; sample < 64; ++sample) {
const int frame_sample = timeslot * 64 + sample;
float gain = phase_new;
if (ramp_phase && frame_sample < 256) {
const float product = static_cast<float>(static_cast<float>(frame_sample) * phase_step);
gain = static_cast<float>(phase_old + product);
}
scaled[sample] = static_cast<float>(bed5[channel * 1536 + frame_sample] * gain);
}
const float* analysis_input = scaled;
if (channel < 3) {
std::memcpy(delayed, analysis_delay_[channel][0], sizeof(delayed));
std::memmove(&analysis_delay_[channel][0][0],
&analysis_delay_[channel][1][0],
sizeof(float) * 9 * 64);
std::memcpy(analysis_delay_[channel][9], scaled, sizeof(scaled));
analysis_input = delayed;
}
analysis_slot(channel, analysis_input, timeslot);
}
}
void analysis(const float* bed5, const float phase_new) noexcept {
job_bed5_ = bed5;
job_phase_old_ = analysis_phase_;
job_phase_new_ = phase_new;
job_kind_ = JobKind::Analysis;
job_count_ = 5;
for (int channel = 0; channel < 5; ++channel) {
job_objects_[channel] = channel;
}
dispatch_jobs();
analysis_phase_ = phase_new;
surround_post();
}
static std::size_t dq_index(const int object, const int point, const int channel, const int band) noexcept {
return static_cast<std::size_t>((((object * 2 + point) * 5 + channel) * kMaxBands) + band);
}
void matrix_object(
const int object,
const uint8_t* n_bands,
const uint8_t* n_dpoints,
const uint8_t* slope_idx,
const uint8_t* offset_ts,
const double* dq) noexcept {
std::memset(z_[object], 0, sizeof(z_[object]));
const int bands = n_bands[object];
const int points = n_dpoints[object];
const int slope = slope_idx[object];
const uint8_t* pb_map = parameter_band_map(bands);
for (int channel = 0; channel < 5; ++channel) {
for (int subband = 0; subband < 64; ++subband) {
const int parameter_band = pb_map[subband];
const double previous = matrix_previous_[object][channel][subband];
const double target0 = dq[dq_index(object, 0, channel, parameter_band)];
const double target1 = points == 2
? dq[dq_index(object, 1, channel, parameter_band)]
: target0;
double last = previous;
for (int timeslot = 0; timeslot < 24; ++timeslot) {
double coefficient;
if (slope == 0) {
if (points == 1) {
const double alpha = static_cast<double>(timeslot + 1) / 24.0;
coefficient = previous * (1.0 - alpha) + target0 * alpha;
} else if (timeslot < 12) {
const double alpha = static_cast<double>(timeslot + 1) / 12.0;
coefficient = previous * (1.0 - alpha) + target0 * alpha;
} else {
const double alpha = static_cast<double>(timeslot - 11) / 12.0;
coefficient = target0 * (1.0 - alpha) + target1 * alpha;
}
} else if (points == 1) {
coefficient = timeslot < offset_ts[object * 2] ? previous : target0;
} else {
coefficient = timeslot < offset_ts[object * 2] ? previous : target0;
if (timeslot >= offset_ts[object * 2 + 1]) {
coefficient = target1;
}
}
z_[object][subband][timeslot].re += x_[channel][subband][timeslot].re * coefficient;
z_[object][subband][timeslot].im += x_[channel][subband][timeslot].im * coefficient;
last = coefficient;
}
matrix_previous_[object][channel][subband] = last;
}
}
}
void qmf5_step(double* state, const double* rotated, double* output) noexcept {
for (int block = 0; block < 16; ++block) {
for (int lane = 0; lane < 4; ++lane) {
const int sample = block * 4 + lane;
const int state_base = block * 36 + lane;
const double even = rotated[block * 8 + lane * 2];
const double odd = rotated[block * 8 + lane * 2 + 1];
output[sample] = 2.0 * (
tables::kQmf5Window[sample] * even + state[state_base]);
state[state_base] = tables::kQmf5Window[1 * 64 + sample] * odd + state[state_base + 4];
for (int slot = 0; slot < 7; ++slot) {
const double alternating = (slot & 1) == 0 ? even : odd;
state[state_base + (slot + 1) * 4] =
tables::kQmf5Window[(slot + 2) * 64 + sample] * alternating +
state[state_base + (slot + 2) * 4];
}
state[state_base + 8 * 4] = tables::kQmf5Window[9 * 64 + sample] * odd;
}
}
}
void synthesis_object(
const int object,
const double clipgain,
const float output_scale,
float* output16) noexcept {
Complex frequency[64];
double rotated[128];
double pcm64[64];
double* state = synthesis_state_[object];
float* destination = output16 + (object + 1) * 1536;
for (int timeslot = 0; timeslot < 24; ++timeslot) {
for (int k = 0; k < 32; ++k) {
const Complex even = z_[object][2 * k][timeslot];
const Complex odd = z_[object][2 * k + 1][timeslot];
frequency[k] = {even.re, -even.im};
frequency[63 - k] = {odd.re, odd.im};
}
fft64(frequency);
for (int k = 0; k < 64; ++k) {
const double re = frequency[k].re;
const double im = frequency[k].im;
const double sin_component = rotation_sin_[k];
const double cos_component = rotation_cos_[k];
rotated[2 * k] = 2.0 * (re * cos_component + im * sin_component);
rotated[2 * k + 1] = 2.0 * (im * cos_component - re * sin_component);
}
qmf5_step(state, rotated, pcm64);
for (int sample = 0; sample < 64; ++sample) {
const double clipped = clamp_unit(16.0 * pcm64[sample]);
const float value = static_cast<float>(clipped * clipgain);
destination[timeslot * 64 + sample] = static_cast<float>(value * output_scale);
}
}
}
void process_one_object(const int object) noexcept {
matrix_object(object, job_n_bands_, job_n_dpoints_, job_slope_idx_,
job_offset_ts_, job_dq_);
synthesis_object(object, job_clipgain_, job_output_scale_, job_output16_);
}
void execute_job_loop() noexcept {
while (true) {
const int index = next_job_.fetch_add(1, std::memory_order_relaxed);
if (index >= job_count_) {
break;
}
const int item = job_objects_[index];
if (job_kind_ == JobKind::Analysis) {
analysis_channel(item);
} else {
process_one_object(item);
}
}
}
void dispatch_jobs() noexcept {
if (workers_.empty() || job_count_ == 1) {
next_job_.store(0, std::memory_order_relaxed);
execute_job_loop();
return;
}
next_job_.store(0, std::memory_order_relaxed);
work_barrier_->arrive_and_wait();
execute_job_loop();
work_barrier_->arrive_and_wait();
}
void worker_loop() noexcept {
while (!pool_ready_.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
if (stop_.load(std::memory_order_acquire)) {
return;
}
while (true) {
work_barrier_->arrive_and_wait();
if (stop_.load(std::memory_order_acquire)) {
return;
}
execute_job_loop();
work_barrier_->arrive_and_wait();
}
}
void stop_workers() noexcept {
if (workers_.empty()) {
work_barrier_.reset();
stop_.store(false, std::memory_order_relaxed);
pool_ready_.store(false, std::memory_order_relaxed);
return;
}
stop_.store(true, std::memory_order_release);
work_barrier_->arrive_and_wait();
for (std::thread& worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
workers_.clear();
work_barrier_.reset();
stop_.store(false, std::memory_order_relaxed);
pool_ready_.store(false, std::memory_order_relaxed);
}
void process_objects(
const uint32_t object_mask,
const uint8_t* n_bands,
const uint8_t* n_dpoints,
const uint8_t* slope_idx,
const uint8_t* offset_ts,
const double* dq,
const double clipgain,
const float output_scale,
float* output16) noexcept {
int count = 0;
for (int object = 0; object < EJOC_MAX_OBJECTS; ++object) {
if (object_mask & (1u << object)) {
job_objects_[count++] = object;
}
}
if (count == 0) {
return;
}
job_kind_ = JobKind::Objects;
job_count_ = count;
job_n_bands_ = n_bands;
job_n_dpoints_ = n_dpoints;
job_slope_idx_ = slope_idx;
job_offset_ts_ = offset_ts;
job_dq_ = dq;
job_clipgain_ = clipgain;
job_output_scale_ = output_scale;
job_output16_ = output16;
dispatch_jobs();
}
void render_lfe(const float* lfe, const float output_scale, float* output16) noexcept {
if (!lfe) {
return;
}
for (int sample = 0; sample < kLfeDelay; ++sample) {
const float value = static_cast<float>(clamp_unit(lfe_delay_[sample]));
output16[sample] = static_cast<float>(value * output_scale);
}
for (int sample = kLfeDelay; sample < 1536; ++sample) {
const float value = static_cast<float>(clamp_unit(static_cast<double>(lfe[sample - kLfeDelay])));
output16[sample] = static_cast<float>(value * output_scale);
}
for (int sample = 0; sample < kLfeDelay; ++sample) {
lfe_delay_[sample] = static_cast<double>(lfe[sample + (1536 - kLfeDelay)]);
}
}
enum class JobKind : uint8_t { Analysis, Objects };
std::vector<std::thread> workers_;
std::unique_ptr<std::barrier<>> work_barrier_;
std::atomic<bool> stop_{false};
std::atomic<bool> pool_ready_{false};
std::atomic<int> next_job_{0};
int job_objects_[EJOC_MAX_OBJECTS]{};
int job_count_ = 0;
JobKind job_kind_ = JobKind::Objects;
const float* job_bed5_ = nullptr;
float job_phase_old_ = 0.0625f;
float job_phase_new_ = 0.0625f;
const uint8_t* job_n_bands_ = nullptr;
const uint8_t* job_n_dpoints_ = nullptr;
const uint8_t* job_slope_idx_ = nullptr;
const uint8_t* job_offset_ts_ = nullptr;
const double* job_dq_ = nullptr;
double job_clipgain_ = 1.0;
float job_output_scale_ = 1.0f;
float* job_output16_ = nullptr;
alignas(64) double analysis_fifo_[5][9][64];
alignas(64) float analysis_delay_[3][10][64];
float analysis_phase_;
alignas(64) Complex surround_delay_[2][10][64];
alignas(64) Complex surround_history_[2][20];
alignas(64) double lfe_delay_[kLfeDelay];
alignas(64) double matrix_previous_[15][5][64];
alignas(64) double synthesis_state_[15][640];
alignas(64) Complex x_[5][64][24];
alignas(64) Complex z_[15][64][24];
uint8_t bit_reverse_[64];
Complex fft_twiddle_[32];
double rotation_sin_[64];
double rotation_cos_[64];
char error_[256];
};
} // namespace ejoc
extern "C" {
uint32_t EJOC_CALL ejoc_abi_version(void) {
return EJOC_ABI_VERSION;
}
const char* EJOC_CALL ejoc_build_info(void) {
#if defined(_MSC_VER)
return "eac3joc-core abi=1 compiler=MSVC fft=fixed64 speaker=double crt=static-by-build";
#elif defined(__clang__)
return "eac3joc-core abi=1 compiler=Clang fft=fixed64 speaker=double";
#elif defined(__GNUC__)
return "eac3joc-core abi=1 compiler=GCC fft=fixed64 speaker=double";
#else
return "eac3joc-core abi=1 compiler=unknown fft=fixed64 speaker=double";
#endif
}
ejoc_renderer_handle EJOC_CALL ejoc_renderer_create(void) {
return new (std::nothrow) ejoc::Renderer();
}
void EJOC_CALL ejoc_renderer_destroy(ejoc_renderer_handle handle) {
delete static_cast<ejoc::Renderer*>(handle);
}
int EJOC_CALL ejoc_renderer_reset(ejoc_renderer_handle handle) {
if (!handle) {
return -1;
}
return static_cast<ejoc::Renderer*>(handle)->reset();
}
int EJOC_CALL ejoc_renderer_set_threads(ejoc_renderer_handle handle, uint32_t total_threads) {
if (!handle) {
return -1;
}
return static_cast<ejoc::Renderer*>(handle)->set_threads(total_threads);
}
uint32_t EJOC_CALL ejoc_renderer_thread_count(ejoc_renderer_handle handle) {
if (!handle) {
return 0;
}
return static_cast<ejoc::Renderer*>(handle)->thread_count();
}
const char* EJOC_CALL ejoc_renderer_last_error(ejoc_renderer_handle handle) {
if (!handle) {
return "null renderer handle";
}
return static_cast<ejoc::Renderer*>(handle)->error();
}
int EJOC_CALL ejoc_renderer_process(
ejoc_renderer_handle handle,
const float* bed5_planar,
const float* lfe,
const uint32_t object_mask,
const uint8_t* n_bands,
const uint8_t* n_dpoints,
const uint8_t* slope_idx,
const uint8_t* offset_ts,
const double* dq,
const double clipgain,
const float phase_new,
const float output_scale,
float* output16_planar) {
if (!handle) {
return -1;
}
return static_cast<ejoc::Renderer*>(handle)->process(
bed5_planar, lfe, object_mask, n_bands, n_dpoints, slope_idx,
offset_ts, dq, clipgain, phase_new, output_scale, output16_planar);
}
} // extern "C"
+89
View File
@@ -0,0 +1,89 @@
#pragma once
// JOC Huffman trees required by the bitstream parser.
static const int joc_huff_code_coarse_generic[][2] =
{
{ -1, 1}, { 2, -2}, { -96, 3}, { 4, -3}, { -95, 5}, { 6, 7}, { -4, -94}, { 8, 9}, { -5, -93}, { 10, 11},
{ -6, -92}, { 12, 13}, { -7, -91}, { 14, 15}, { 16, -90}, { -8, 17}, { 18, -89}, { -9, 19}, { 20, 21}, { -88, -10},
{ 22, 23}, { -11, -87}, { 24, 25}, { 26, -86}, { -12, 27}, { 28, -85}, { -13, 29}, { 30, 31}, { 32, -84}, { -14, 33},
{ 34, -15}, { -83, 35}, { 36, 37}, { -16, 38}, { -17, -82}, { 39, 40}, { 41, -81}, { 42, 43}, { 44, 45}, { 46, 47},
{ 48, 49}, { 50, 51}, { 52, -18}, { -78, 53}, { -19, 54}, { 55, 56}, { 57, 58}, { -22, 59}, { 60, 61}, { 62, 63},
{ 64, 65}, { 66, 67}, { 68, -20}, { -21, -79}, { -80, -25}, { 69, 70}, { -26, 71}, { 72, 73}, { 74, 75}, { 76, 77},
{ 78, 79}, { 80, 81}, { 82, 83}, { 84, 85}, { 86, 87}, { 88, 89}, { 90, 91}, { 92, 93}, { 94, -23}, { -74, -75},
{ -72, -73}, { -76, -77}, { -34, -35}, { -32, -33}, { -38, -39}, { -36, -37}, { -30, -31}, { -28, -29}, { -50, -51}, { -48, -49},
{ -54, -55}, { -52, -53}, { -42, -43}, { -40, -41}, { -46, -47}, { -44, -45}, { -66, -67}, { -64, -65}, { -70, -71}, { -68, -69},
{ -58, -59}, { -56, -57}, { -62, -63}, { -60, -61}, { -24, -27}
};
static const int joc_huff_code_fine_generic[][2] =
{
{ -1, 1}, { 2, 3}, { -2,-192}, { 4, 5}, { 6, -3}, {-191, 7}, { 8, 9}, { -4,-190}, { 10, 11}, { -5,-189},
{ 12, 13}, { -6, 14}, {-188, 15}, { 16, -7}, {-187, 17}, { 18, -8}, {-186, 19}, { 20, -9}, {-185, 21}, { 22, -10},
{-184, 23}, { 24, -11}, { 25,-183}, { 26, 27}, { -12,-182}, { 28, 29}, { -13,-181}, { 30, 31}, {-180, -14}, { 32, 33},
{ 34,-179}, { -15, 35}, { 36,-178}, { -16, 37}, { 38,-177}, { 39, -17}, { 40, 41}, {-176, 42}, { -18, 43}, { -19, 44},
{-175, 45}, { 46,-174}, { -20, 47}, {-173, 48}, { 49, -21}, { 50, 51}, { 52, -22}, { 53, 54}, {-172, 55}, {-171, -23},
{ 56, 57}, { 58,-170}, { 59, -24}, { -25, 60}, {-169, 61}, { 62, 63}, { 64, 65}, { 66, 67}, {-168, 68}, { -26, 69},
{-167, -27}, { 70,-166}, {-165, 71}, { -29, 72}, { 73, 74}, { -30, 75}, { 76, 77}, { 78, 79}, { 80, -28}, { 81, 82},
{ 83,-163}, { -31, -33}, {-164,-161}, { 84, 85}, { 86, 87}, { 88, 89}, { 90, 91}, { 92, 93}, { 94, 95}, { 96, 97},
{ 98, 99}, { -32,-162}, { 100, 101}, { 102, 103}, { 104, 105}, { 106, 107}, { 108, 109}, { 110, 111}, {-160, 112}, { -36, -38},
{ 113, 114}, { 115, 116}, { 117, 118}, { 119, 120}, { 121, 122}, { 123, 124}, { 125, 126}, { 127, 128}, { 129, 130}, { 131, 132},
{ 133, -35}, {-158, 134}, {-155,-156}, { -37, -42}, { 135, 136}, { 137, 138}, { 139, 140}, { 141, 142}, { 143, 144}, { 145, 146},
{ 147, 148}, { 149, 150}, { 151, 152}, { 153, 154}, { 155, 156}, { 157, 158}, { 159, 160}, { 161, 162}, { 163, 164}, { 165, 166},
{ 167, 168}, { 169, 170}, { 171, 172}, { 173, 174}, { 175, 176}, { 177, 178}, { 179, 180}, { 181, 182}, { 183, 184}, {-157, 185},
{ -45, -48}, { 186, 187}, { 188, 189}, { -34, -41}, { 190, -39}, { -60, -61}, { -58, -59}, { -64, -65}, { -62, -63}, { -52, -53},
{ -50, -51}, { -56, -57}, { -54, -55}, { -76, -77}, { -74, -75}, { -80, -81}, { -78, -79}, { -68, -69}, { -66, -67}, { -72, -73},
{ -70, -71}, { -47, -49}, { -44, -46}, {-124,-125}, {-122,-123}, {-128,-129}, {-126,-127}, {-116,-117}, {-114,-115}, {-120,-121},
{-118,-119}, {-140,-141}, {-138,-139}, {-144,-145}, {-142,-143}, {-132,-133}, {-130,-131}, {-136,-137}, {-134,-135}, { -92, -93},
{ -90, -91}, { -96, -97}, { -94, -95}, { -84, -85}, { -82, -83}, { -88, -89}, { -86, -87}, {-108,-109}, {-106,-107}, {-112,-113},
{-110,-111}, {-100,-101}, { -98, -99}, {-104,-105}, {-102,-103}, {-154,-159}, {-148,-149}, {-146,-147}, {-152,-153}, {-150,-151},
{ -40, -43}
};
static const int joc_huff_code_coarse_coeff_sparse[][2] =
{
{ -1, 1}, { 2, 3}, { -2, -96}, { 4, 5}, { 6, -95}, { -3, 7}, { 8, 9}, { -4, 10}, { -94, 11}, { 12, -5},
{ -93, 13}, { 14, 15}, { -6, -92}, { 16, 17}, { 18, -7}, { -91, 19}, { 20, -8}, { -90, 21}, { 22, 23}, { -9, -89},
{ 24, 25}, { 26, -10}, { -88, 27}, { 28, 29}, { 30, -11}, { -87, 31}, { 32, 33}, { 34, 35}, { -12, -86}, { 36, 37},
{ 38, -13}, { 39, -85}, { 40, 41}, { 42, 43}, { -14, -84}, { 44, 45}, { 46, 47}, { -83, -15}, { 48, 49}, { 50, -16},
{ 51, 52}, { -82, 53}, { 54, -81}, { 55, 56}, { -17, 57}, { 58, -80}, { 59, 60}, { -18, 61}, { 62, 63}, { -79, 64},
{ -19, -78}, { 65, 66}, { 67, 68}, { 69, -20}, { -77, -21}, { 70, 71}, { 72, 73}, { 74, -76}, { 75, -22}, { 76, 77},
{ -75, 78}, { 79, 80}, { -54, -74}, { -73, 81}, { -23, 82}, { -50, -24}, { -55, -25}, { 83, -47}, { -49, -44}, { -71, 84},
{ -48, -51}, { 85, -72}, { -26, -53}, { -70, -27}, { 86, -45}, { 87, 88}, { -68, 89}, { -29, -43}, { 90, -30}, { -46, -69},
{ 91, -28}, { -52, -31}, { 92, -32}, { 93, -64}, { -67, 94}, { -36, -33}, { -63, -37}, { -65, -61}, { -66, -59}, { -34, -38},
{ -41, -42}, { -35, -60}, { -39, -57}, { -56, -40}, { -62, -58}
};
static const int joc_huff_code_fine_coeff_sparse[][2] =
{
{ 1, -1}, { 2, 3}, { 4, -2}, {-192, 5}, { 6, 7}, { 8, -3}, {-191, 9}, { 10, 11}, { 12,-190}, { -4, 13},
{ 14, 15}, {-189, -5}, { 16, 17}, { 18, -6}, {-188, 19}, { 20, 21}, { -7,-187}, { 22, 23}, { -8, 24}, {-186, 25},
{ -9, 26}, { 27,-185}, { 28, -10}, { 29, 30}, {-184, 31}, { -11, 32}, { 33,-183}, { 34, -12}, { 35,-182}, { 36, 37},
{ 38, -13}, {-181, 39}, { 40, -14}, { 41,-180}, { 42, 43}, {-179, -15}, { 44, -16}, { 45,-178}, { 46, 47}, { 48, 49},
{ 50,-177}, { -17, 51}, { -18, 52}, {-176, 53}, { 54, 55}, {-175, -19}, { 56, 57}, { 58, -20}, { 59,-174}, { 60, 61},
{ -21, 62}, { 63,-173}, { 64, 65}, { 66,-172}, { 67, 68}, { -22, 69}, { 70, 71}, { -23, 72}, {-171, 73}, { 74, 75},
{ 76, -24}, { 77,-170}, { -25, 78}, { 79, 80}, { 81,-169}, { 82, 83}, { 84, -26}, { 85,-168}, { 86, 87}, { 88, 89},
{-167, 90}, { -27, 91}, { 92, -28}, { 93,-166}, { 94, -29}, { 95, 96}, { 97, 98}, {-165, 99}, { 100, -30}, {-164, 101},
{ 102, 103}, { 104, 105}, {-163, 106}, { -31, 107}, { -32, 108}, { 109, 110}, {-161, 111}, {-160,-162}, { 112, -34}, { -33, 113},
{ 114, 115}, { 116, 117}, { 118, 119}, { 120,-159}, { 121, 122}, { 123,-158}, { 124, 125}, { -36,-155}, { 126, 127}, { -35, 128},
{ 129, 130}, {-157, 131}, {-156, 132}, { -37, 133}, { 134, 135}, {-154, -38}, { 136, 137}, { -39, -41}, { 138,-153}, { 139, -40},
{-149, 140}, { 141, 142}, { 143, 144}, {-151, 145}, { 146, 147}, { 148, -42}, { -43, 149}, { 150, 151}, {-152, 152}, { -46, -98},
{ 153, 154}, { 155,-147}, { 156, 157}, { 158,-107}, { 159, 160}, {-145,-150}, { -96, 161}, { 162, -45}, {-146, 163}, { 164, -97},
{-108,-105}, {-148,-106}, { -44, 165}, { -94,-141}, { -99, 166}, { -89, 167}, { -50, -95}, {-100, -48}, {-144, 168}, { 169, 170},
{ -51,-142}, { -90, -91}, { -47, -49}, { 171, -53}, { -93,-143}, {-137,-138}, { -55,-101}, { 172, 173}, { -54, -86}, { -88, -87},
{-103, 174}, { 175, -61}, {-109, 176}, { 177, 178}, { -52,-139}, { -57,-140}, { 179, 180}, { -56,-136}, { -58,-102}, { 181, 182},
{ -60,-135}, { 183,-104}, {-128,-134}, { -92, 184}, { -59, -62}, { 185, 186}, { -71,-133}, { 187,-127}, {-126, 188}, { -63, -64},
{ -85,-132}, { 189, -66}, {-121,-125}, { 190, -68}, { -74, -75}, { -70, -73}, { -81, -65}, {-118,-131}, { -72,-110}, {-119,-120},
{ -76, -84}, {-122,-130}, { -83,-117}, { -69, -78}, { -80, -82}, {-123,-124}, { -67,-116}, {-129, -77}, {-113,-114}, {-112,-115},
{ -79,-111}
};
static const int joc_huff_code_5ch_pos_index_sparse[][2] =
{
{ -1, 1}, { 2, 3}, { -4, -3}, { -2, -5}
};
static const int joc_huff_code_7ch_pos_index_sparse[][2] =
{
{ -1, 1}, { 2, 3}, { 4, 5}, { -4, -3}, { -2, -5}, { -6, -7}
};
+388
View File
@@ -0,0 +1,388 @@
#pragma once
#include <stdint.h>
// Generated table data; do not hand-edit.
namespace ejoc::tables {
inline constexpr double kAnalysisWindow[640] = {
0.00019903187057934701, 0.00024947625934146345, 0.00030217695166356862, 0.00035484600812196732,
0.0004058915947098285, 0.00045464080176316202, 0.00050126801943406463, 0.00054649583762511611,
0.00059120741207152605, 0.0006361178238876164, 0.00068160606315359473, 0.00072772568091750145,
0.00077434181002900004, 0.00082129903603345156, 0.00086853635730221868, 0.00091610715026035905,
0.00096411682898178697, 0.0010126305278390646, 0.0010616052895784378, 0.0011108826147392392,
0.0011602368904277682, 0.0012094489065930247, 0.0012583627831190825, 0.0013069023843854666,
0.0013550462899729609, 0.0014027846045792103, 0.0014500867109745741, 0.0014968989416956902,
0.0015431707724928856, 0.0015888890484347939, 0.0016340982401743531, 0.0016788924112915993,
0.0017233812250196934, 0.0017676511779427528, 0.0018117419676855206, 0.0018556505674496293,
0.0018993609119206667, 0.0019428766099736094, 0.0019862416666001081, 0.0020295341964811087,
0.0020728406962007284, 0.0021162291523069143, 0.0021597379818558693, 0.0022033930290490389,
0.0022472396958619356, 0.0022913739085197449, 0.0023359460756182671, 0.0023811329156160355,
0.0024270866997539997, 0.0024738919455558062, 0.0025215502828359604, 0.0025700139813125134,
0.0026192441582679749, 0.0026692659594118595, 0.002720177173614502, 0.0027720888610929251,
0.0028250094037503004, 0.0028787164483219385, 0.0029326770454645157, 0.0029860674403607845,
0.0030379060190171003, 0.0030872693751007318, 0.0031335193198174238, 0.0031764607410877943,
0.0032163741998374462, 0.0032539025414735079, 0.0032898378558456898, 0.0033248732797801495,
0.003359407652169466, 0.0033934540115296841, 0.0034266682341694832, 0.0034584659151732922,
0.0034881711471825838, 0.0035151413176208735, 0.0035388274118304253, 0.0035587677266448736,
0.0035745392087846994, 0.0035856980830430984, 0.0035917432978749275, 0.0035921167582273483,
0.0035862282384186983, 0.0035734928678721189, 0.0035533567424863577, 0.0035253004170954227,
0.0034888240043073893, 0.003443423192948103, 0.0033885682933032513, 0.0033236993476748466,
0.0032482317183166742, 0.0031615688931196928, 0.0030631136614829302, 0.0029522709082812071,
0.0028284420259296894, 0.0026910160668194294, 0.0025393660180270672, 0.0023728485684841871,
0.0021908141206949949, 0.0019926181994378567, 0.0017776311142370105, 0.0015452421503141522,
0.001294855959713459, 0.0010258855763822794, 0.00073774566408246756, 0.00042984966421499848,
0.00010161137470277026, -0.00024754938203841448, -0.00061819725669920444, -0.0010108760325238109,
-0.001426108181476593, -0.0018643926596269011, -0.0023262077011168003, -0.002812013728544116,
-0.0033222525380551815, -0.003857344388961792, -0.0044176783412694931, -0.0050036045722663403,
-0.0056154225021600723, -0.0062533821910619736, -0.0069176913239061832, -0.0076085370965301991,
-0.008326113224029541, -0.0090706516057252884, -0.0098424339666962624, -0.010641784407198429,
-0.011469035409390926, -0.012324465438723564, -0.013208229094743729, -0.014120301231741905,
0.015060451813042164, 0.016028247773647308, 0.017023105174303055, 0.018044359982013702,
0.019091326743364334, 0.020163353532552719, 0.021259821951389313, 0.022380130365490913,
0.02352365106344223, 0.024689681828022003, 0.025877414271235466, 0.027085918933153152,
0.028314167633652687, 0.029561035335063934, 0.030825328081846237, 0.032105788588523865,
0.033401083201169968, 0.034709792584180832, 0.036030396819114685, 0.037361271679401398,
0.03870067372918129, 0.040046781301498413, 0.041397668421268463, 0.042751342058181763,
0.044105727225542068, 0.045458663254976273, 0.046807888895273209, 0.048151064664125443,
0.049485750496387482, 0.050809424370527267, 0.05211947113275528, 0.053413204848766327,
0.054687850177288055, 0.055940557271242142, 0.057168368250131607, 0.058368254452943802,
0.059537097811698914, 0.06067170575261116, 0.061768818646669388, 0.062825113534927368,
0.063837200403213501, 0.064801648259162903, 0.065714947879314423, 0.066573545336723328,
0.067373812198638916, 0.068112112581729889, 0.068784743547439575, 0.069387979805469513,
0.069918066263198853, 0.070371203124523163, 0.070743560791015625, 0.071031264960765839,
0.071230456233024597, 0.071337237954139709, 0.071347743272781372, 0.071258097887039185,
0.07106444239616394, 0.070762887597084045, 0.070349536836147308, 0.069820456206798553,
0.069171726703643799, 0.068399444222450256, 0.067499779164791107, 0.066468983888626099,
0.065303429961204529, 0.063999593257904053, 0.062554046511650085, 0.060963429510593414,
0.059224434196949005, 0.057333782315254211, 0.055288247764110565, 0.05308464914560318,
0.050719890743494034, 0.04819098487496376, 0.045495055615901947, 0.042629346251487732,
0.039591230452060699, 0.036378197371959686, 0.032987859100103378, 0.029417969286441803,
0.025666400790214539, 0.0217311792075634, 0.017610486596822739, 0.013302664272487164,
0.0088062174618244171, 0.0041198157705366611, -0.00075770384864881635, -0.0058273370377719402,
-0.011089906096458435, -0.016546055674552917, -0.022196246311068535, -0.028040755540132523,
-0.034079667180776596, -0.040312871336936951, -0.046740073710680008, -0.053360763937234879,
-0.06017424538731575, -0.0671796053647995, -0.074375726282596588, -0.081761270761489868,
-0.089334696531295776, -0.097094230353832245, -0.10503791272640228, -0.1131635457277298,
-0.12146873027086258, -0.12995083630084991, -0.13860704004764557, -0.1474342942237854,
-0.15642932057380676, -0.1655886322259903, -0.17490856349468231, -0.18438516557216644,
-0.19401434063911438, -0.20379173755645752, -0.21371282637119293, -0.22377283871173859,
-0.23396681249141693, -0.24428960680961609, -0.25473582744598389, -0.26529994606971741,
-0.27597621083259583, -0.2867586612701416, -0.29764124751091003, -0.30861768126487732,
-0.31968152523040771, -0.33082622289657593, -0.34204500913619995, -0.35333094000816345,
0.36467701196670532, 0.37607598304748535, 0.38752046227455139, 0.39900293946266174,
0.41051584482192993, 0.42205137014389038, 0.4336017370223999, 0.44515895843505859,
0.45671501755714417, 0.46826183795928955, 0.47979119420051575, 0.49129492044448853,
0.50276464223861694, 0.51419198513031006, 0.52556860446929932, 0.53688603639602661,
0.54813587665557861, 0.55930960178375244, 0.57039880752563477, 0.58139497041702271,
0.59228962659835815, 0.60307443141937256, 0.61374092102050781, 0.62428075075149536,
0.63468557596206665, 0.64494723081588745, 0.65505754947662354, 0.66500836610794067,
0.67479169368743896, 0.68439966440200806, 0.69382447004318237, 0.70305842161178589,
0.71209394931793213, 0.72092366218566895, 0.72954028844833374, 0.73793661594390869,
0.74610573053359985, 0.75404083728790283, 0.76173526048660278, 0.76918256282806396,
0.77637648582458496, 0.78331100940704346, 0.78998017311096191, 0.79637837409973145,
0.80250018835067749, 0.80834043025970459, 0.81389403343200684, 0.81915634870529175,
0.82412278652191162, 0.82878917455673218, 0.83315145969390869, 0.83720588684082031,
0.84094899892807007, 0.84437751770019531, 0.84748858213424683, 0.85027939081192017,
0.85274755954742432, 0.85489106178283691, 0.85670793056488037, 0.8581966757774353,
0.85935592651367188, 0.86018466949462891, 0.86068224906921387, 0.86084812879562378,
0.86068224906921387, 0.86018466949462891, 0.85935592651367188, 0.8581966757774353,
0.85670793056488037, 0.85489106178283691, 0.85274755954742432, 0.85027939081192017,
0.84748858213424683, 0.84437751770019531, 0.84094899892807007, 0.83720588684082031,
0.83315145969390869, 0.82878917455673218, 0.82412278652191162, 0.81915634870529175,
0.81389403343200684, 0.80834043025970459, 0.80250018835067749, 0.79637837409973145,
0.78998017311096191, 0.78331100940704346, 0.77637648582458496, 0.76918256282806396,
0.76173526048660278, 0.75404083728790283, 0.74610573053359985, 0.73793661594390869,
0.72954028844833374, 0.72092366218566895, 0.71209394931793213, 0.70305842161178589,
0.69382447004318237, 0.68439966440200806, 0.67479169368743896, 0.66500836610794067,
0.65505754947662354, 0.64494723081588745, 0.63468557596206665, 0.62428075075149536,
0.61374092102050781, 0.60307443141937256, 0.59228962659835815, 0.58139497041702271,
0.57039880752563477, 0.55930960178375244, 0.54813587665557861, 0.53688603639602661,
0.52556860446929932, 0.51419198513031006, 0.50276464223861694, 0.49129492044448853,
0.47979119420051575, 0.46826183795928955, 0.45671501755714417, 0.44515895843505859,
0.4336017370223999, 0.42205137014389038, 0.41051584482192993, 0.39900293946266174,
0.38752046227455139, 0.37607598304748535, 0.36467701196670532, 0.35333094000816345,
-0.34204500913619995, -0.33082622289657593, -0.31968152523040771, -0.30861768126487732,
-0.29764124751091003, -0.2867586612701416, -0.27597621083259583, -0.26529994606971741,
-0.25473582744598389, -0.24428960680961609, -0.23396681249141693, -0.22377283871173859,
-0.21371282637119293, -0.20379173755645752, -0.19401434063911438, -0.18438516557216644,
-0.17490856349468231, -0.1655886322259903, -0.15642932057380676, -0.1474342942237854,
-0.13860704004764557, -0.12995083630084991, -0.12146873027086258, -0.1131635457277298,
-0.10503791272640228, -0.097094230353832245, -0.089334696531295776, -0.081761270761489868,
-0.074375726282596588, -0.0671796053647995, -0.06017424538731575, -0.053360763937234879,
-0.046740073710680008, -0.040312871336936951, -0.034079667180776596, -0.028040755540132523,
-0.022196246311068535, -0.016546055674552917, -0.011089906096458435, -0.0058273370377719402,
-0.00075770384864881635, 0.0041198157705366611, 0.0088062174618244171, 0.013302664272487164,
0.017610486596822739, 0.0217311792075634, 0.025666400790214539, 0.029417969286441803,
0.032987859100103378, 0.036378197371959686, 0.039591230452060699, 0.042629346251487732,
0.045495055615901947, 0.04819098487496376, 0.050719890743494034, 0.05308464914560318,
0.055288247764110565, 0.057333782315254211, 0.059224434196949005, 0.060963429510593414,
0.062554046511650085, 0.063999593257904053, 0.065303429961204529, 0.066468983888626099,
0.067499779164791107, 0.068399444222450256, 0.069171726703643799, 0.069820456206798553,
0.070349536836147308, 0.070762887597084045, 0.07106444239616394, 0.071258097887039185,
0.071347743272781372, 0.071337237954139709, 0.071230456233024597, 0.071031264960765839,
0.070743560791015625, 0.070371203124523163, 0.069918066263198853, 0.069387979805469513,
0.068784743547439575, 0.068112112581729889, 0.067373812198638916, 0.066573545336723328,
0.065714947879314423, 0.064801648259162903, 0.063837200403213501, 0.062825113534927368,
0.061768818646669388, 0.06067170575261116, 0.059537097811698914, 0.058368254452943802,
0.057168368250131607, 0.055940557271242142, 0.054687850177288055, 0.053413204848766327,
0.05211947113275528, 0.050809424370527267, 0.049485750496387482, 0.048151064664125443,
0.046807888895273209, 0.045458663254976273, 0.044105727225542068, 0.042751342058181763,
0.041397668421268463, 0.040046781301498413, 0.03870067372918129, 0.037361271679401398,
0.036030396819114685, 0.034709792584180832, 0.033401083201169968, 0.032105788588523865,
0.030825328081846237, 0.029561035335063934, 0.028314167633652687, 0.027085918933153152,
0.025877414271235466, 0.024689681828022003, 0.02352365106344223, 0.022380130365490913,
0.021259821951389313, 0.020163353532552719, 0.019091326743364334, 0.018044359982013702,
0.017023105174303055, 0.016028247773647308, 0.015060451813042164, 0.014120301231741905,
-0.013208229094743729, -0.012324465438723564, -0.011469035409390926, -0.010641784407198429,
-0.0098424339666962624, -0.0090706516057252884, -0.008326113224029541, -0.0076085370965301991,
-0.0069176913239061832, -0.0062533821910619736, -0.0056154225021600723, -0.0050036045722663403,
-0.0044176783412694931, -0.003857344388961792, -0.0033222525380551815, -0.002812013728544116,
-0.0023262077011168003, -0.0018643926596269011, -0.001426108181476593, -0.0010108760325238109,
-0.00061819725669920444, -0.00024754938203841448, 0.00010161137470277026, 0.00042984966421499848,
0.00073774566408246756, 0.0010258855763822794, 0.001294855959713459, 0.0015452421503141522,
0.0017776311142370105, 0.0019926181994378567, 0.0021908141206949949, 0.0023728485684841871,
0.0025393660180270672, 0.0026910160668194294, 0.0028284420259296894, 0.0029522709082812071,
0.0030631136614829302, 0.0031615688931196928, 0.0032482317183166742, 0.0033236993476748466,
0.0033885682933032513, 0.003443423192948103, 0.0034888240043073893, 0.0035253004170954227,
0.0035533567424863577, 0.0035734928678721189, 0.0035862282384186983, 0.0035921167582273483,
0.0035917432978749275, 0.0035856980830430984, 0.0035745392087846994, 0.0035587677266448736,
0.0035388274118304253, 0.0035151413176208735, 0.0034881711471825838, 0.0034584659151732922,
0.0034266682341694832, 0.0033934540115296841, 0.003359407652169466, 0.0033248732797801495,
0.0032898378558456898, 0.0032539025414735079, 0.0032163741998374462, 0.0031764607410877943,
0.0031335193198174238, 0.0030872693751007318, 0.0030379060190171003, 0.0029860674403607845,
0.0029326770454645157, 0.0028787164483219385, 0.0028250094037503004, 0.0027720888610929251,
0.002720177173614502, 0.0026692659594118595, 0.0026192441582679749, 0.0025700139813125134,
0.0025215502828359604, 0.0024738919455558062, 0.0024270866997539997, 0.0023811329156160355,
0.0023359460756182671, 0.0022913739085197449, 0.0022472396958619356, 0.0022033930290490389,
0.0021597379818558693, 0.0021162291523069143, 0.0020728406962007284, 0.0020295341964811087,
0.0019862416666001081, 0.0019428766099736094, 0.0018993609119206667, 0.0018556505674496293,
0.0018117419676855206, 0.0017676511779427528, 0.0017233812250196934, 0.0016788924112915993,
0.0016340982401743531, 0.0015888890484347939, 0.0015431707724928856, 0.0014968989416956902,
0.0014500867109745741, 0.0014027846045792103, 0.0013550462899729609, 0.0013069023843854666,
0.0012583627831190825, 0.0012094489065930247, 0.0011602368904277682, 0.0011108826147392392,
0.0010616052895784378, 0.0010126305278390646, 0.00096411682898178697, 0.00091610715026035905,
0.00086853635730221868, 0.00082129903603345156, 0.00077434181002900004, 0.00072772568091750145,
0.00068160606315359473, 0.0006361178238876164, 0.00059120741207152605, 0.00054649583762511611,
0.00050126801943406463, 0.00045464080176316202, 0.0004058915947098285, 0.00035484600812196732,
0.00030217695166356862, 0.00024947625934146345, 0.00019903187057934701, 0,
};
inline constexpr double kQmf5Window[640] = {
0, 0.00019903187057934701, 0.00024947625934146345, 0.00030217695166356862,
0.00035484600812196732, 0.0004058915947098285, 0.00045464080176316202, 0.00050126801943406463,
0.00054649583762511611, 0.00059120741207152605, 0.0006361178238876164, 0.00068160606315359473,
0.00072772568091750145, 0.00077434181002900004, 0.00082129903603345156, 0.00086853635730221868,
0.00091610715026035905, 0.00096411682898178697, 0.0010126305278390646, 0.0010616052895784378,
0.0011108826147392392, 0.0011602368904277682, 0.0012094489065930247, 0.0012583627831190825,
0.0013069023843854666, 0.0013550462899729609, 0.0014027846045792103, 0.0014500867109745741,
0.0014968989416956902, 0.0015431707724928856, 0.0015888890484347939, 0.0016340982401743531,
0.0016788924112915993, 0.0017233812250196934, 0.0017676511779427528, 0.0018117419676855206,
0.0018556505674496293, 0.0018993609119206667, 0.0019428766099736094, 0.0019862416666001081,
0.0020295341964811087, 0.0020728406962007284, 0.0021162291523069143, 0.0021597379818558693,
0.0022033930290490389, 0.0022472396958619356, 0.0022913739085197449, 0.0023359460756182671,
0.0023811329156160355, 0.0024270866997539997, 0.0024738919455558062, 0.0025215502828359604,
0.0025700139813125134, 0.0026192441582679749, 0.0026692659594118595, 0.002720177173614502,
0.0027720888610929251, 0.0028250094037503004, 0.0028787164483219385, 0.0029326770454645157,
0.0029860674403607845, 0.0030379060190171003, 0.0030872693751007318, 0.0031335193198174238,
0.0031764607410877943, 0.0032163741998374462, 0.0032539025414735079, 0.0032898378558456898,
0.0033248732797801495, 0.003359407652169466, 0.0033934540115296841, 0.0034266682341694832,
0.0034584659151732922, 0.0034881711471825838, 0.0035151413176208735, 0.0035388274118304253,
0.0035587677266448736, 0.0035745392087846994, 0.0035856980830430984, 0.0035917432978749275,
0.0035921167582273483, 0.0035862282384186983, 0.0035734928678721189, 0.0035533567424863577,
0.0035253004170954227, 0.0034888240043073893, 0.003443423192948103, 0.0033885682933032513,
0.0033236993476748466, 0.0032482317183166742, 0.0031615688931196928, 0.0030631136614829302,
0.0029522709082812071, 0.0028284420259296894, 0.0026910160668194294, 0.0025393660180270672,
0.0023728485684841871, 0.0021908141206949949, 0.0019926181994378567, 0.0017776311142370105,
0.0015452421503141522, 0.001294855959713459, 0.0010258855763822794, 0.00073774566408246756,
0.00042984966421499848, 0.00010161137470277026, -0.00024754938203841448, -0.00061819725669920444,
-0.0010108760325238109, -0.001426108181476593, -0.0018643926596269011, -0.0023262077011168003,
-0.002812013728544116, -0.0033222525380551815, -0.003857344388961792, -0.0044176783412694931,
-0.0050036045722663403, -0.0056154225021600723, -0.0062533821910619736, -0.0069176913239061832,
-0.0076085370965301991, -0.008326113224029541, -0.0090706516057252884, -0.0098424339666962624,
-0.010641784407198429, -0.011469035409390926, -0.012324465438723564, -0.013208229094743729,
0.014120301231741905, 0.015060451813042164, 0.016028247773647308, 0.017023105174303055,
0.018044359982013702, 0.019091326743364334, 0.020163353532552719, 0.021259821951389313,
0.022380130365490913, 0.02352365106344223, 0.024689681828022003, 0.025877414271235466,
0.027085918933153152, 0.028314167633652687, 0.029561035335063934, 0.030825328081846237,
0.032105788588523865, 0.033401083201169968, 0.034709792584180832, 0.036030396819114685,
0.037361271679401398, 0.03870067372918129, 0.040046781301498413, 0.041397668421268463,
0.042751342058181763, 0.044105727225542068, 0.045458663254976273, 0.046807888895273209,
0.048151064664125443, 0.049485750496387482, 0.050809424370527267, 0.05211947113275528,
0.053413204848766327, 0.054687850177288055, 0.055940557271242142, 0.057168368250131607,
0.058368254452943802, 0.059537097811698914, 0.06067170575261116, 0.061768818646669388,
0.062825113534927368, 0.063837200403213501, 0.064801648259162903, 0.065714947879314423,
0.066573545336723328, 0.067373812198638916, 0.068112112581729889, 0.068784743547439575,
0.069387979805469513, 0.069918066263198853, 0.070371203124523163, 0.070743560791015625,
0.071031264960765839, 0.071230456233024597, 0.071337237954139709, 0.071347743272781372,
0.071258097887039185, 0.07106444239616394, 0.070762887597084045, 0.070349536836147308,
0.069820456206798553, 0.069171726703643799, 0.068399444222450256, 0.067499779164791107,
0.066468983888626099, 0.065303429961204529, 0.063999593257904053, 0.062554046511650085,
0.060963429510593414, 0.059224434196949005, 0.057333782315254211, 0.055288247764110565,
0.05308464914560318, 0.050719890743494034, 0.04819098487496376, 0.045495055615901947,
0.042629346251487732, 0.039591230452060699, 0.036378197371959686, 0.032987859100103378,
0.029417969286441803, 0.025666400790214539, 0.0217311792075634, 0.017610486596822739,
0.013302664272487164, 0.0088062174618244171, 0.0041198157705366611, -0.00075770384864881635,
-0.0058273370377719402, -0.011089906096458435, -0.016546055674552917, -0.022196246311068535,
-0.028040755540132523, -0.034079667180776596, -0.040312871336936951, -0.046740073710680008,
-0.053360763937234879, -0.06017424538731575, -0.0671796053647995, -0.074375726282596588,
-0.081761270761489868, -0.089334696531295776, -0.097094230353832245, -0.10503791272640228,
-0.1131635457277298, -0.12146873027086258, -0.12995083630084991, -0.13860704004764557,
-0.1474342942237854, -0.15642932057380676, -0.1655886322259903, -0.17490856349468231,
-0.18438516557216644, -0.19401434063911438, -0.20379173755645752, -0.21371282637119293,
-0.22377283871173859, -0.23396681249141693, -0.24428960680961609, -0.25473582744598389,
-0.26529994606971741, -0.27597621083259583, -0.2867586612701416, -0.29764124751091003,
-0.30861768126487732, -0.31968152523040771, -0.33082622289657593, -0.34204500913619995,
0.35333094000816345, 0.36467701196670532, 0.37607598304748535, 0.38752046227455139,
0.39900293946266174, 0.41051584482192993, 0.42205137014389038, 0.4336017370223999,
0.44515895843505859, 0.45671501755714417, 0.46826183795928955, 0.47979119420051575,
0.49129492044448853, 0.50276464223861694, 0.51419198513031006, 0.52556860446929932,
0.53688603639602661, 0.54813587665557861, 0.55930960178375244, 0.57039880752563477,
0.58139497041702271, 0.59228962659835815, 0.60307443141937256, 0.61374092102050781,
0.62428075075149536, 0.63468557596206665, 0.64494723081588745, 0.65505754947662354,
0.66500836610794067, 0.67479169368743896, 0.68439966440200806, 0.69382447004318237,
0.70305842161178589, 0.71209394931793213, 0.72092366218566895, 0.72954028844833374,
0.73793661594390869, 0.74610573053359985, 0.75404083728790283, 0.76173526048660278,
0.76918256282806396, 0.77637648582458496, 0.78331100940704346, 0.78998017311096191,
0.79637837409973145, 0.80250018835067749, 0.80834043025970459, 0.81389403343200684,
0.81915634870529175, 0.82412278652191162, 0.82878917455673218, 0.83315145969390869,
0.83720588684082031, 0.84094899892807007, 0.84437751770019531, 0.84748858213424683,
0.85027939081192017, 0.85274755954742432, 0.85489106178283691, 0.85670793056488037,
0.8581966757774353, 0.85935592651367188, 0.86018466949462891, 0.86068224906921387,
0.86084812879562378, 0.86068224906921387, 0.86018466949462891, 0.85935592651367188,
0.8581966757774353, 0.85670793056488037, 0.85489106178283691, 0.85274755954742432,
0.85027939081192017, 0.84748858213424683, 0.84437751770019531, 0.84094899892807007,
0.83720588684082031, 0.83315145969390869, 0.82878917455673218, 0.82412278652191162,
0.81915634870529175, 0.81389403343200684, 0.80834043025970459, 0.80250018835067749,
0.79637837409973145, 0.78998017311096191, 0.78331100940704346, 0.77637648582458496,
0.76918256282806396, 0.76173526048660278, 0.75404083728790283, 0.74610573053359985,
0.73793661594390869, 0.72954028844833374, 0.72092366218566895, 0.71209394931793213,
0.70305842161178589, 0.69382447004318237, 0.68439966440200806, 0.67479169368743896,
0.66500836610794067, 0.65505754947662354, 0.64494723081588745, 0.63468557596206665,
0.62428075075149536, 0.61374092102050781, 0.60307443141937256, 0.59228962659835815,
0.58139497041702271, 0.57039880752563477, 0.55930960178375244, 0.54813587665557861,
0.53688603639602661, 0.52556860446929932, 0.51419198513031006, 0.50276464223861694,
0.49129492044448853, 0.47979119420051575, 0.46826183795928955, 0.45671501755714417,
0.44515895843505859, 0.4336017370223999, 0.42205137014389038, 0.41051584482192993,
0.39900293946266174, 0.38752046227455139, 0.37607598304748535, 0.36467701196670532,
-0.35333094000816345, -0.34204500913619995, -0.33082622289657593, -0.31968152523040771,
-0.30861768126487732, -0.29764124751091003, -0.2867586612701416, -0.27597621083259583,
-0.26529994606971741, -0.25473582744598389, -0.24428960680961609, -0.23396681249141693,
-0.22377283871173859, -0.21371282637119293, -0.20379173755645752, -0.19401434063911438,
-0.18438516557216644, -0.17490856349468231, -0.1655886322259903, -0.15642932057380676,
-0.1474342942237854, -0.13860704004764557, -0.12995083630084991, -0.12146873027086258,
-0.1131635457277298, -0.10503791272640228, -0.097094230353832245, -0.089334696531295776,
-0.081761270761489868, -0.074375726282596588, -0.0671796053647995, -0.06017424538731575,
-0.053360763937234879, -0.046740073710680008, -0.040312871336936951, -0.034079667180776596,
-0.028040755540132523, -0.022196246311068535, -0.016546055674552917, -0.011089906096458435,
-0.0058273370377719402, -0.00075770384864881635, 0.0041198157705366611, 0.0088062174618244171,
0.013302664272487164, 0.017610486596822739, 0.0217311792075634, 0.025666400790214539,
0.029417969286441803, 0.032987859100103378, 0.036378197371959686, 0.039591230452060699,
0.042629346251487732, 0.045495055615901947, 0.04819098487496376, 0.050719890743494034,
0.05308464914560318, 0.055288247764110565, 0.057333782315254211, 0.059224434196949005,
0.060963429510593414, 0.062554046511650085, 0.063999593257904053, 0.065303429961204529,
0.066468983888626099, 0.067499779164791107, 0.068399444222450256, 0.069171726703643799,
0.069820456206798553, 0.070349536836147308, 0.070762887597084045, 0.07106444239616394,
0.071258097887039185, 0.071347743272781372, 0.071337237954139709, 0.071230456233024597,
0.071031264960765839, 0.070743560791015625, 0.070371203124523163, 0.069918066263198853,
0.069387979805469513, 0.068784743547439575, 0.068112112581729889, 0.067373812198638916,
0.066573545336723328, 0.065714947879314423, 0.064801648259162903, 0.063837200403213501,
0.062825113534927368, 0.061768818646669388, 0.06067170575261116, 0.059537097811698914,
0.058368254452943802, 0.057168368250131607, 0.055940557271242142, 0.054687850177288055,
0.053413204848766327, 0.05211947113275528, 0.050809424370527267, 0.049485750496387482,
0.048151064664125443, 0.046807888895273209, 0.045458663254976273, 0.044105727225542068,
0.042751342058181763, 0.041397668421268463, 0.040046781301498413, 0.03870067372918129,
0.037361271679401398, 0.036030396819114685, 0.034709792584180832, 0.033401083201169968,
0.032105788588523865, 0.030825328081846237, 0.029561035335063934, 0.028314167633652687,
0.027085918933153152, 0.025877414271235466, 0.024689681828022003, 0.02352365106344223,
0.022380130365490913, 0.021259821951389313, 0.020163353532552719, 0.019091326743364334,
0.018044359982013702, 0.017023105174303055, 0.016028247773647308, 0.015060451813042164,
-0.014120301231741905, -0.013208229094743729, -0.012324465438723564, -0.011469035409390926,
-0.010641784407198429, -0.0098424339666962624, -0.0090706516057252884, -0.008326113224029541,
-0.0076085370965301991, -0.0069176913239061832, -0.0062533821910619736, -0.0056154225021600723,
-0.0050036045722663403, -0.0044176783412694931, -0.003857344388961792, -0.0033222525380551815,
-0.002812013728544116, -0.0023262077011168003, -0.0018643926596269011, -0.001426108181476593,
-0.0010108760325238109, -0.00061819725669920444, -0.00024754938203841448, 0.00010161137470277026,
0.00042984966421499848, 0.00073774566408246756, 0.0010258855763822794, 0.001294855959713459,
0.0015452421503141522, 0.0017776311142370105, 0.0019926181994378567, 0.0021908141206949949,
0.0023728485684841871, 0.0025393660180270672, 0.0026910160668194294, 0.0028284420259296894,
0.0029522709082812071, 0.0030631136614829302, 0.0031615688931196928, 0.0032482317183166742,
0.0033236993476748466, 0.0033885682933032513, 0.003443423192948103, 0.0034888240043073893,
0.0035253004170954227, 0.0035533567424863577, 0.0035734928678721189, 0.0035862282384186983,
0.0035921167582273483, 0.0035917432978749275, 0.0035856980830430984, 0.0035745392087846994,
0.0035587677266448736, 0.0035388274118304253, 0.0035151413176208735, 0.0034881711471825838,
0.0034584659151732922, 0.0034266682341694832, 0.0033934540115296841, 0.003359407652169466,
0.0033248732797801495, 0.0032898378558456898, 0.0032539025414735079, 0.0032163741998374462,
0.0031764607410877943, 0.0031335193198174238, 0.0030872693751007318, 0.0030379060190171003,
0.0029860674403607845, 0.0029326770454645157, 0.0028787164483219385, 0.0028250094037503004,
0.0027720888610929251, 0.002720177173614502, 0.0026692659594118595, 0.0026192441582679749,
0.0025700139813125134, 0.0025215502828359604, 0.0024738919455558062, 0.0024270866997539997,
0.0023811329156160355, 0.0023359460756182671, 0.0022913739085197449, 0.0022472396958619356,
0.0022033930290490389, 0.0021597379818558693, 0.0021162291523069143, 0.0020728406962007284,
0.0020295341964811087, 0.0019862416666001081, 0.0019428766099736094, 0.0018993609119206667,
0.0018556505674496293, 0.0018117419676855206, 0.0017676511779427528, 0.0017233812250196934,
0.0016788924112915993, 0.0016340982401743531, 0.0015888890484347939, 0.0015431707724928856,
0.0014968989416956902, 0.0014500867109745741, 0.0014027846045792103, 0.0013550462899729609,
0.0013069023843854666, 0.0012583627831190825, 0.0012094489065930247, 0.0011602368904277682,
0.0011108826147392392, 0.0010616052895784378, 0.0010126305278390646, 0.00096411682898178697,
0.00091610715026035905, 0.00086853635730221868, 0.00082129903603345156, 0.00077434181002900004,
0.00072772568091750145, 0.00068160606315359473, 0.0006361178238876164, 0.00059120741207152605,
0.00054649583762511611, 0.00050126801943406463, 0.00045464080176316202, 0.0004058915947098285,
0.00035484600812196732, 0.00030217695166356862, 0.00024947625934146345, 0.00019903187057934701,
};
inline constexpr uint8_t kPbMap1[64] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
inline constexpr uint8_t kPbMap3[64] = {
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
};
inline constexpr uint8_t kPbMap5[64] = {
0, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
};
inline constexpr uint8_t kPbMap7[64] = {
0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5,
5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
};
inline constexpr uint8_t kPbMap9[64] = {
0, 1, 2, 3, 3, 3, 4, 5, 5, 6, 6, 6, 7, 7, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
};
inline constexpr uint8_t kPbMap12[64] = {
0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8,
8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
};
inline constexpr uint8_t kPbMap15[64] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 10, 10, 11, 11, 11,
12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
};
inline constexpr uint8_t kPbMap23[64] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 12, 13, 13,
14, 14, 15, 15, 16, 16, 16, 17, 17, 17, 18, 18, 18, 18, 19, 19,
19, 19, 19, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
};
} // namespace ejoc::tables
+534
View File
@@ -0,0 +1,534 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
namespace ejoc::speaker_tables {
inline constexpr std::size_t kMaxPoints = 15;
inline constexpr std::size_t kMaxGroups = 4;
inline constexpr std::size_t kMaxGroupSize = 3;
inline constexpr std::size_t kMaxChannels = 16;
struct SpeakerPoint {
std::array<std::uint16_t, 3> coordinate_q15{};
std::uint8_t speaker_id{};
};
struct AxisGroup {
std::uint8_t size{};
std::array<std::uint8_t, kMaxGroupSize> indices{};
};
struct RegionGeometry {
std::uint8_t point_count{};
std::uint8_t mode{};
std::uint8_t axis0_group_count{};
std::uint8_t axis1_group_count{};
std::array<SpeakerPoint, kMaxPoints> points{};
std::array<AxisGroup, kMaxGroups> axis0_groups{};
std::array<AxisGroup, kMaxGroups> axis1_groups{};
};
struct LayoutGeometry {
std::uint32_t speaker_bitfield{};
std::uint8_t out_ch_config{};
std::uint8_t channel_count{};
std::array<std::uint8_t, kMaxChannels> standard_from_internal{};
std::array<RegionGeometry, 7> regions{};
};
inline constexpr std::array<LayoutGeometry, 10> kLayouts{{
LayoutGeometry{
0x1u, 0, 2,
{{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0x7u, 3, 4,
{{0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
1, 1, 1, 0,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0xFu, 7, 6,
{{0, 1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
5, 2, 2, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 2, 2, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 2, 2, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 2, 2, 0,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0x1Fu, 11, 8,
{{0, 1, 2, 3, 6, 7, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
7, 2, 3, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 2, 2, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 2, 2, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 2, 2, 0,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
4, 2, 2, 0,
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0x40Fu, 13, 8,
{{0, 1, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 2, 1,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 1, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
4, 3, 1, 1,
{{SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0xA0Fu, 14, 10,
{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 2,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 1, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
6, 3, 1, 2,
{{SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {2, 3, 0}}, AxisGroup{2, {4, 5, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 1, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0x41Fu, 15, 10,
{{0, 1, 2, 3, 6, 7, 4, 5, 8, 9, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
9, 3, 3, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}}},
{{AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 2, 1,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 1, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
6, 3, 2, 1,
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {4, 5, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0xA1Fu, 16, 12,
{{0, 1, 2, 3, 6, 7, 4, 5, 8, 9, 10, 11, 0, 0, 0, 0}},
{{
RegionGeometry{
11, 3, 3, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}}},
{{AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {9, 10, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 2,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 1, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
8, 3, 2, 2,
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {4, 5, 0}}, AxisGroup{2, {6, 7, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 1, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0xA9Fu, 19, 14,
{{0, 1, 2, 3, 6, 7, 4, 5, 10, 11, 12, 13, 8, 9, 0, 0}},
{{
RegionGeometry{
13, 3, 4, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}}},
{{AxisGroup{2, {9, 10, 0}}, AxisGroup{2, {11, 12, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
11, 3, 3, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}}},
{{AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {9, 10, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 2,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 1, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
8, 3, 2, 2,
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {4, 5, 0}}, AxisGroup{2, {6, 7, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0xE9Fu, 20, 16,
{{0, 1, 2, 3, 6, 7, 4, 5, 10, 11, 14, 15, 12, 13, 8, 9}},
{{
RegionGeometry{
15, 3, 4, 3,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}}},
{{AxisGroup{2, {9, 10, 0}}, AxisGroup{2, {11, 12, 0}}, AxisGroup{2, {13, 14, 0}}, AxisGroup{}}}
},
RegionGeometry{
13, 3, 3, 3,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}}},
{{AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {9, 10, 0}}, AxisGroup{2, {11, 12, 0}}, AxisGroup{}}}
},
RegionGeometry{
11, 3, 2, 3,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {9, 10, 0}}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 3,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 1, 3,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}}}
},
RegionGeometry{
10, 3, 2, 3,
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {4, 5, 0}}, AxisGroup{2, {6, 7, 0}}, AxisGroup{2, {8, 9, 0}}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
}
}};
inline constexpr const LayoutGeometry* find_layout(const std::uint32_t speaker_bitfield) noexcept {
for (const auto& layout : kLayouts) {
if (layout.speaker_bitfield == speaker_bitfield) {
return &layout;
}
}
return nullptr;
}
} // namespace ejoc::speaker_tables
+495
View File
@@ -0,0 +1,495 @@
#define EJOC_BUILD_DLL
#include "eac3joc_core.h"
#include "speaker_layouts.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <new>
namespace ejoc::speaker {
using speaker_tables::AxisGroup;
using speaker_tables::LayoutGeometry;
using speaker_tables::RegionGeometry;
constexpr double kPi = 3.141592653589793238462643383279502884;
constexpr double kQ15Scale = 32768.0;
constexpr double kQ15Max = 32767.0 / kQ15Scale;
constexpr double kGainSnapThreshold = 1.0e-4;
constexpr std::size_t kObjects = EJOC_MAX_OBJECTS;
constexpr std::size_t kChannels = EJOC_OUTPUT_CHANNELS;
constexpr std::size_t kBlock = EJOC_SPEAKER_BLOCK_SAMPLES;
using PointGains = std::array<double, speaker_tables::kMaxPoints>;
using ChannelGains = std::array<double, kChannels>;
using ObjectChannelGains = std::array<ChannelGains, kObjects>;
using RemainingCounts = std::array<std::array<std::uint32_t, kChannels>, kObjects>;
inline double clamp(const double value, const double low, const double high) noexcept {
return value < low ? low : (value > high ? high : value);
}
inline double coordinate(const RegionGeometry& region, const std::size_t point,
const std::size_t component) noexcept {
return static_cast<double>(region.points[point].coordinate_q15[component]) / kQ15Scale;
}
std::uint64_t expand_speaker_bitfield(const std::uint32_t compact) noexcept {
constexpr std::array<std::uint64_t, 22> expansions{{
0x00000003ULL, 0x00000004ULL, 0x00000008ULL, 0x00000030ULL,
0x000000C0ULL, 0x00000100ULL, 0x00000600ULL, 0x00001800ULL,
0x00006000ULL, 0x00018000ULL, 0x00060000ULL, 0x00180000ULL,
0x00600000ULL, 0x01800000ULL, 0x06000000ULL, 0x18000000ULL,
0x60000000ULL, 0x080000000ULL, 0x600000000ULL, 0x800000000ULL,
0x1000000000ULL, 0x2000000000ULL,
}};
std::uint64_t expanded = 0;
for (std::size_t bit_index = 0; bit_index < expansions.size(); ++bit_index) {
if ((compact & (1u << bit_index)) != 0) {
expanded |= expansions[bit_index];
}
}
return expanded;
}
inline int bit(const std::uint64_t value, const unsigned index) noexcept {
return static_cast<int>((value >> index) & 1ULL);
}
double layout_attenuation_db(const std::uint32_t compact) noexcept {
const std::uint64_t expanded = expand_speaker_bitfield(compact);
const int height_channels = 2 * (
bit(expanded, 13) + bit(expanded, 15) + bit(expanded, 17) +
bit(expanded, 19) + bit(expanded, 21));
const int floor_channels = bit(expanded, 8) + 2 * (
bit(expanded, 31) + bit(expanded, 4) + bit(expanded, 6) +
bit(expanded, 11) + bit(expanded, 25) + bit(expanded, 27) +
bit(expanded, 29) + bit(expanded, 33));
const double height_factor = std::min(static_cast<double>(height_channels) / 4.0, 1.0);
const double floor_factor = std::min(static_cast<double>(floor_channels) / 4.0, 1.0);
return -std::max(4.5 - 1.5 * height_factor - 3.0 * floor_factor, 0.0);
}
int floor_y_exponent(const std::uint32_t compact) noexcept {
const std::uint32_t low = static_cast<std::uint32_t>(expand_speaker_bitfield(compact));
return ((low & 0x130u) != 0 && (low & 0x18C0u) == 0) ? 1 : 0;
}
inline void equal_power_pair(const double position, double& lower, double& upper) noexcept {
const double angle = (kPi * 0.5) * position;
lower = std::cos(angle);
upper = std::sin(angle);
}
void axis0_gains(const RegionGeometry& region,
const std::array<AxisGroup, speaker_tables::kMaxGroups>& groups,
const std::uint8_t group_count,
const double value,
PointGains& output) noexcept {
output.fill(0.0);
for (std::size_t row = 0; row < group_count; ++row) {
const auto& group = groups[row];
if (group.size == 0) {
continue;
}
const std::size_t first = group.indices[0];
const std::size_t last = group.indices[group.size - 1];
const double first_value = coordinate(region, first, 0);
const double last_value = coordinate(region, last, 0);
if (value <= first_value) {
output[first] = 1.0;
continue;
}
if (value >= last_value) {
output[last] = 1.0;
continue;
}
for (std::size_t index = 0; index + 1 < group.size; ++index) {
const std::size_t lower_index = group.indices[index];
const std::size_t upper_index = group.indices[index + 1];
const double lower_value = coordinate(region, lower_index, 0);
const double upper_value = coordinate(region, upper_index, 0);
if (value > lower_value && value <= upper_value) {
const double position = (value - lower_value) / (upper_value - lower_value);
equal_power_pair(position, output[lower_index], output[upper_index]);
break;
}
}
}
}
void axis1_gains(const RegionGeometry& region,
const std::array<AxisGroup, speaker_tables::kMaxGroups>& groups,
const std::uint8_t group_count,
const double value,
PointGains& output) noexcept {
output.fill(0.0);
if (group_count == 0) {
return;
}
const auto& first_group = groups[0];
const auto& last_group = groups[group_count - 1];
const double first_value = coordinate(region, first_group.indices[0], 1);
const double last_value = coordinate(region, last_group.indices[0], 1);
if (value <= first_value) {
for (std::size_t index = 0; index < first_group.size; ++index) {
output[first_group.indices[index]] = 1.0;
}
return;
}
if (value > last_value) {
for (std::size_t index = 0; index < last_group.size; ++index) {
output[last_group.indices[index]] = 1.0;
}
return;
}
for (std::size_t row = 0; row + 1 < group_count; ++row) {
const auto& lower_group = groups[row];
const auto& upper_group = groups[row + 1];
const double lower_value = coordinate(region, lower_group.indices[0], 1);
const double upper_value = coordinate(region, upper_group.indices[0], 1);
if (value >= lower_value && value <= upper_value) {
const double position = (value - lower_value) / (upper_value - lower_value);
double lower_gain = 0.0;
double upper_gain = 0.0;
equal_power_pair(position, lower_gain, upper_gain);
for (std::size_t index = 0; index < lower_group.size; ++index) {
output[lower_group.indices[index]] = lower_gain;
}
for (std::size_t index = 0; index < upper_group.size; ++index) {
output[upper_group.indices[index]] = upper_gain;
}
return;
}
}
}
void plane_gains(const RegionGeometry& region,
const std::array<AxisGroup, speaker_tables::kMaxGroups>& groups,
const std::uint8_t group_count,
const double u,
const double v,
const std::uint8_t mode,
PointGains& output) noexcept {
axis0_gains(region, groups, group_count, u, output);
if (mode >= 2) {
PointGains vertical{};
axis1_gains(region, groups, group_count, v, vertical);
for (std::size_t point = 0; point < region.point_count; ++point) {
output[point] *= vertical[point];
}
}
}
class Renderer {
public:
explicit Renderer(const LayoutGeometry* layout) noexcept : layout_(layout) {
for (std::size_t standard = 0; standard < layout_->channel_count; ++standard) {
standard_index_for_internal_[layout_->standard_from_internal[standard]] =
static_cast<std::uint8_t>(standard);
}
attenuation_db_ = layout_attenuation_db(layout_->speaker_bitfield);
floor_y_exponent_ = floor_y_exponent(layout_->speaker_bitfield);
reset_state();
}
int reset() noexcept {
reset_state();
error_[0] = '\0';
return 0;
}
const char* last_error() const noexcept {
return error_[0] ? error_.data() : "";
}
int process(const float* input,
const std::uint32_t sample_count,
const std::uint32_t metadata_count,
const std::uint32_t* metadata_offsets,
const std::uint32_t* ramp_durations,
const std::uint16_t* positions_q15,
const std::uint8_t* region_indices,
const std::uint8_t* height_enabled,
const double* object_gains,
double* output) noexcept {
error_[0] = '\0';
if (!input || !output) {
return fail("null PCM pointer passed to ejoc_speaker_renderer_process");
}
if ((sample_count % kBlock) != 0) {
return fail("sample_count must be a multiple of 32");
}
if (metadata_count && (!metadata_offsets || !ramp_durations || !positions_q15)) {
return fail("metadata arrays are null while metadata_count is nonzero");
}
if (!validate_metadata(sample_count, metadata_count, metadata_offsets,
positions_q15, region_indices, object_gains)) {
return -1;
}
std::fill(output, output + static_cast<std::size_t>(sample_count) * layout_->channel_count, 0.0);
const bool has_lfe = (layout_->speaker_bitfield & 0x4u) != 0;
const std::size_t total_blocks = sample_count / kBlock;
std::size_t event = 0;
for (std::size_t block = 0; block < total_blocks; ++block) {
while (event < metadata_count && aligned_block(metadata_offsets[event]) == block) {
apply_event(event, ramp_durations, positions_q15, region_indices,
height_enabled, object_gains);
++event;
}
mix_block(input, output, block, has_lfe);
}
while (event < metadata_count && aligned_block(metadata_offsets[event]) == total_blocks) {
apply_event(event, ramp_durations, positions_q15, region_indices,
height_enabled, object_gains);
++event;
}
if (event != metadata_count) {
return fail("metadata alignment produced an event outside this process call");
}
return 0;
}
private:
void reset_state() noexcept {
for (auto& row : current_) row.fill(0.0);
for (auto& row : target_) row.fill(0.0);
for (auto& row : step_) row.fill(0.0);
for (auto& row : remaining_) row.fill(0);
}
int fail(const char* message) noexcept {
std::snprintf(error_.data(), error_.size(), "%s", message);
return -1;
}
bool validate_metadata(const std::uint32_t sample_count,
const std::uint32_t metadata_count,
const std::uint32_t* metadata_offsets,
const std::uint16_t* positions_q15,
const std::uint8_t* region_indices,
const double* object_gains) noexcept {
for (std::size_t event = 0; event < metadata_count; ++event) {
if (metadata_offsets[event] > sample_count) {
fail("metadata offset exceeds sample_count");
return false;
}
if (event && metadata_offsets[event] < metadata_offsets[event - 1]) {
fail("metadata offsets must be nondecreasing");
return false;
}
for (std::size_t object = 0; object < kObjects; ++object) {
const std::size_t object_event = event * kObjects + object;
if (region_indices && region_indices[object_event] >= 7) {
fail("region index is above 6");
return false;
}
if (object_gains && !std::isfinite(object_gains[object_event])) {
fail("object gain is not finite");
return false;
}
const std::size_t coordinate_base = object_event * EJOC_SPEAKER_COORDINATES;
for (std::size_t component = 0; component < EJOC_SPEAKER_COORDINATES; ++component) {
if (positions_q15[coordinate_base + component] > 32767u) {
fail("Q15 object coordinate is above 32767");
return false;
}
}
}
}
return true;
}
static std::size_t aligned_block(const std::uint32_t sample) noexcept {
return (static_cast<std::size_t>(sample) + kBlock / 2 - 1) / kBlock;
}
static std::uint32_t ramp_blocks(const std::uint32_t duration) noexcept {
return static_cast<std::uint32_t>(
(static_cast<std::size_t>(duration) + kBlock / 2 - 1) / kBlock);
}
void render_point(const std::uint16_t* position,
const std::uint8_t region_index,
const bool enable_height,
const double object_gain,
ChannelGains& output) const noexcept {
output.fill(0.0);
const auto& region = layout_->regions[region_index];
const double u = static_cast<double>(position[0]) / kQ15Scale;
const double v = static_cast<double>(position[1]) / kQ15Scale;
const double w = static_cast<double>(position[2]) / kQ15Scale;
const double floor_v = clamp(std::ldexp(v, floor_y_exponent_), 0.0, 1.0);
PointGains floor{};
plane_gains(region, region.axis0_groups, region.axis0_group_count,
u, floor_v, region.mode, floor);
PointGains point = floor;
if (region.mode == 3) {
PointGains height{};
plane_gains(region, region.axis1_groups, region.axis1_group_count,
u, v, 3, height);
const double z = enable_height ? clamp(w, 0.0, kQ15Max) : 0.0;
if (z >= kQ15Max) {
point = height;
} else if (z > 0.0) {
double floor_weight = 0.0;
double height_weight = 0.0;
equal_power_pair(z, floor_weight, height_weight);
for (std::size_t index = 0; index < region.point_count; ++index) {
point[index] = floor[index] * floor_weight + height[index] * height_weight;
}
}
}
const double y_term = clamp(v / 0.6, 0.0, 1.0);
const double z_term = clamp((w - 0.2) / 0.8, 0.0, 1.0);
const double amount = clamp(y_term + z_term, 0.0, 1.0);
const double gain = std::pow(10.0, attenuation_db_ * amount / 20.0) * object_gain;
for (std::size_t index = 0; index < region.point_count; ++index) {
output[region.points[index].speaker_id] = point[index] * gain;
}
}
void apply_event(const std::size_t event,
const std::uint32_t* ramp_durations,
const std::uint16_t* positions_q15,
const std::uint8_t* region_indices,
const std::uint8_t* height_enabled,
const double* object_gains) noexcept {
const std::uint32_t blocks = ramp_blocks(ramp_durations[event]);
for (std::size_t object = 0; object < kObjects; ++object) {
const std::size_t object_event = event * kObjects + object;
const auto* position = positions_q15 + object_event * EJOC_SPEAKER_COORDINATES;
const std::uint8_t region = region_indices ? region_indices[object_event] : 0;
const bool height = !height_enabled || height_enabled[object_event] != 0;
const double object_gain = object_gains ? object_gains[object_event] : 1.0;
ChannelGains next{};
render_point(position, region, height, object_gain, next);
for (std::size_t channel = 0; channel < layout_->channel_count; ++channel) {
const double difference = next[channel] - current_[object][channel];
target_[object][channel] = next[channel];
if (std::abs(difference) >= kGainSnapThreshold && blocks != 0) {
step_[object][channel] = difference / static_cast<double>(blocks);
remaining_[object][channel] = blocks;
} else {
current_[object][channel] = next[channel];
step_[object][channel] = 0.0;
remaining_[object][channel] = 0;
}
}
}
}
void mix_block(const float* input, double* output, const std::size_t block,
const bool has_lfe) noexcept {
const std::size_t start = block * kBlock;
if (has_lfe) {
for (std::size_t sample = 0; sample < kBlock; ++sample) {
output[(start + sample) * layout_->channel_count + 3] =
static_cast<double>(input[(start + sample) * EJOC_OUTPUT_CHANNELS]);
}
}
for (std::size_t object = 0; object < kObjects; ++object) {
for (std::size_t internal = 0; internal < layout_->channel_count; ++internal) {
const bool active = remaining_[object][internal] != 0;
const double fixed_gain = target_[object][internal];
if (!active && fixed_gain == 0.0) {
continue;
}
const std::size_t standard = standard_index_for_internal_[internal];
for (std::size_t sample = 0; sample < kBlock; ++sample) {
const double gain = active
? current_[object][internal] +
(static_cast<double>(sample) / static_cast<double>(kBlock)) *
step_[object][internal]
: fixed_gain;
output[(start + sample) * layout_->channel_count + standard] +=
static_cast<double>(
input[(start + sample) * EJOC_OUTPUT_CHANNELS + object + 1]) * gain;
}
if (active) {
current_[object][internal] += step_[object][internal];
--remaining_[object][internal];
if (remaining_[object][internal] == 0) {
current_[object][internal] = target_[object][internal];
}
} else {
current_[object][internal] = target_[object][internal];
}
}
}
}
const LayoutGeometry* layout_;
double attenuation_db_{};
int floor_y_exponent_{};
ObjectChannelGains current_{};
ObjectChannelGains target_{};
ObjectChannelGains step_{};
RemainingCounts remaining_{};
std::array<std::uint8_t, kChannels> standard_index_for_internal_{};
std::array<char, 256> error_{};
};
} // namespace ejoc::speaker
extern "C" {
uint32_t EJOC_CALL ejoc_speaker_layout_channel_count(const uint32_t speaker_bitfield) {
const auto* layout = ejoc::speaker_tables::find_layout(speaker_bitfield);
return layout ? layout->channel_count : 0;
}
ejoc_speaker_renderer_handle EJOC_CALL ejoc_speaker_renderer_create(
const uint32_t speaker_bitfield) {
const auto* layout = ejoc::speaker_tables::find_layout(speaker_bitfield);
if (!layout) {
return nullptr;
}
return new (std::nothrow) ejoc::speaker::Renderer(layout);
}
void EJOC_CALL ejoc_speaker_renderer_destroy(ejoc_speaker_renderer_handle handle) {
delete static_cast<ejoc::speaker::Renderer*>(handle);
}
int EJOC_CALL ejoc_speaker_renderer_reset(ejoc_speaker_renderer_handle handle) {
if (!handle) {
return -1;
}
return static_cast<ejoc::speaker::Renderer*>(handle)->reset();
}
const char* EJOC_CALL ejoc_speaker_renderer_last_error(ejoc_speaker_renderer_handle handle) {
if (!handle) {
return "speaker renderer handle is null";
}
return static_cast<ejoc::speaker::Renderer*>(handle)->last_error();
}
int EJOC_CALL ejoc_speaker_renderer_process(
ejoc_speaker_renderer_handle handle,
const float* objects16_interleaved,
const uint32_t sample_count,
const uint32_t metadata_count,
const uint32_t* metadata_offsets,
const uint32_t* ramp_durations,
const uint16_t* positions_q15,
const uint8_t* region_indices,
const uint8_t* height_enabled,
const double* object_gains,
double* output_interleaved) {
if (!handle) {
return -1;
}
return static_cast<ejoc::speaker::Renderer*>(handle)->process(
objects16_interleaved, sample_count, metadata_count,
metadata_offsets, ramp_durations, positions_q15,
region_indices, height_enabled, object_gains, output_interleaved);
}
} // extern "C"
+1
View File
@@ -0,0 +1 @@
numpy>=1.24
+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
+273
View File
@@ -0,0 +1,273 @@
"""生成 25 声道 RF64 ADM BWF 及其 axml、chna、dbmd 元数据。
输出由 10 声道 7.1.2 bed 和 15 路对象组成;RF64 尺寸字段在写入完成后回填。
"""
import struct
import numpy as np
import xml.etree.ElementTree as ET
NS = "urn:ebu:metadata-schema:ebuCore_2016"
XSI = "http://www.w3.org/2001/XMLSchema-instance"
BED_NAMES = ["RoomCentricLeft", "RoomCentricRight", "RoomCentricCenter",
"RoomCentricLFE", "RoomCentricLeftSideSurround",
"RoomCentricRightSideSurround", "RoomCentricLeftRearSurround",
"RoomCentricRightRearSurround", "RoomCentricLeftTopSurround",
"RoomCentricRightTopSurround"]
BED_LABELS = ["RC_L", "RC_R", "RC_C", "RC_LFE", "RC_Lss", "RC_Rss",
"RC_Lrs", "RC_Rrs", "RC_Lts", "RC_Rts"]
BED_POS = [(-1.0, 1.0, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0),
(-1.0, 1.0, -1.0), (-1.0, 0.0, 0.0), (1.0, 0.0, 0.0),
(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (-1.0, 0.0, 1.0), (1.0, 0.0, 1.0)]
N_OBJ = 15
def q_to_adm_xyz(q1, q2, q3):
posX = min(1.0, round(q1 * 62 / 32767.0) / 62.0)
posY = min(1.0, round(q2 * 62 / 32767.0) / 62.0)
posZ = round(q3 * 15 / 32767.0) / 15.0
posZ = max(-1.0, min(1.0, posZ))
return posX * 2 - 1, 1 - posY * 2, posZ
def ts(seconds):
s = int(seconds)
frac = int(round((seconds - s) * 100000))
if frac >= 100000:
s += 1; frac = 0
return f"{s // 3600:02d}:{s % 3600 // 60:02d}:{s % 60:02d}.{frac:05d}"
def sub(parent, tag, attrib=None, text=None):
e = ET.SubElement(parent, tag)
if attrib:
for k, v in attrib.items():
e.set(k, v)
if text is not None:
e.text = text
return e
def add_refs(parent, tag, ids):
for i in ids:
sub(parent, tag, text=i)
def obj_block(cf, bid, t, x, y, z, dur, interpolation=0.0):
b = sub(cf, "audioBlockFormat", {
"audioBlockFormatID": bid, "rtime": ts(t), "duration": ts(dur)})
sub(b, "cartesian", text="1")
for c, v in (("X", x), ("Y", y), ("Z", z)):
if c == "Z" and v == 0:
continue
p = sub(b, "position", {"coordinate": c})
p.text = f"{v:.10f}"
sub(b, "jumpPosition", {"interpolationLength": f"{interpolation:.5f}"}, text="1")
def build_axml(obj_tracks, duration_sec):
adm = ET.Element("ebuCoreMain", {
"xmlns": NS, "xmlns:xsi": XSI,
"xsi:schemaLocation": f"{NS} ebucore.xsd", "lang": "en"})
core = sub(adm, "coreMetadata")
fmt = sub(core, "format")
af = sub(fmt, "audioFormatExtended")
prog = sub(af, "audioProgramme", {
"audioProgrammeID": "APR_1001", "audioProgrammeName": "EAC3JOC_Export",
"start": ts(0), "end": ts(duration_sec)})
add_refs(prog, "audioContentIDRef", ("ACO_1001", "ACO_1002"))
bc = sub(af, "audioContent", {"audioContentID": "ACO_1001",
"audioContentName": "EAC3JOC_Master_Content"})
add_refs(bc, "audioObjectIDRef", ["AO_1001"])
sub(bc, "dialogue", {"mixedContentKind": "0"})
oc = sub(af, "audioContent", {"audioContentID": "ACO_1002",
"audioContentName": "Objects"})
add_refs(oc, "audioObjectIDRef", ["AO_%04x" % (0x100b + i) for i in range(N_OBJ)])
sub(oc, "dialogue", {"mixedContentKind": "0"})
bed_o = sub(af, "audioObject", {"audioObjectID": "AO_1001", "audioObjectName": "Bed",
"start": ts(0), "duration": ts(duration_sec)})
sub(bed_o, "audioPackFormatIDRef", text="AP_00011001")
add_refs(bed_o, "audioTrackUIDRef", ["ATU_%08x" % (i + 1) for i in range(10)])
for i in range(N_OBJ):
o = sub(af, "audioObject", {"audioObjectID": "AO_%04x" % (0x100b + i),
"audioObjectName": f"Audio Object {i+1}",
"start": ts(0), "duration": ts(duration_sec)})
sub(o, "audioPackFormatIDRef", text="AP_0003%04x" % (0x1001 + i))
add_refs(o, "audioTrackUIDRef", ["ATU_%08x" % (i + 11)])
bp = sub(af, "audioPackFormat", {"audioPackFormatID": "AP_00011001",
"audioPackFormatName": "EAC3JOCBedPack",
"typeDefinition": "DirectSpeakers", "typeLabel": "0001"})
add_refs(bp, "audioChannelFormatIDRef", ["AC_0001%04x" % (0x1001 + i) for i in range(10)])
for i in range(N_OBJ):
pk = sub(af, "audioPackFormat", {"audioPackFormatID": "AP_0003%04x" % (0x1001 + i),
"audioPackFormatName": f"JOC_Object_{i+1}",
"typeDefinition": "Objects", "typeLabel": "0003"})
add_refs(pk, "audioChannelFormatIDRef", ["AC_0003%04x" % (0x1001 + i)])
for i in range(10):
cf = sub(af, "audioChannelFormat", {"audioChannelFormatID": "AC_0001%04x" % (0x1001 + i),
"audioChannelFormatName": BED_NAMES[i],
"typeDefinition": "DirectSpeakers", "typeLabel": "0001"})
b = sub(cf, "audioBlockFormat", {"audioBlockFormatID": "AB_0001%04x_00000001" % (0x1001 + i)})
sub(b, "cartesian", text="1")
x, y, z = BED_POS[i]
for c, v in (("X", x), ("Y", y), ("Z", z)):
if c == "Z" and v == 0:
continue
p = sub(b, "position", {"coordinate": c})
p.text = f"{v:.10f}"
sub(b, "speakerLabel", text=BED_LABELS[i])
for i, (oname, kfs) in enumerate(obj_tracks):
cf = sub(af, "audioChannelFormat", {"audioChannelFormatID": "AC_0003%04x" % (0x1001 + i),
"audioChannelFormatName": oname,
"typeDefinition": "Objects", "typeLabel": "0003"})
for k, keyframe in enumerate(kfs):
t, x, y, z, dur = keyframe[:5]
interpolation = keyframe[5] if len(keyframe) > 5 else 0.0
obj_block(cf, "AB_0003%04x_%08x" % (0x1001 + i, k + 1),
t, x, y, z, dur, interpolation)
for i in range(10):
t = sub(af, "audioTrackUID", {"UID": "ATU_%08x" % (i + 1),
"bitDepth": "24", "sampleRate": "48000"})
sub(t, "audioTrackFormatIDRef", text="AT_0001%04x_01" % (0x1001 + i))
sub(t, "audioPackFormatIDRef", text="AP_00011001")
for i in range(N_OBJ):
t = sub(af, "audioTrackUID", {"UID": "ATU_%08x" % (i + 11),
"bitDepth": "24", "sampleRate": "48000"})
sub(t, "audioTrackFormatIDRef", text="AT_0003%04x_01" % (0x1001 + i))
sub(t, "audioPackFormatIDRef", text="AP_0003%04x" % (0x1001 + i))
for i in range(10):
tf = sub(af, "audioTrackFormat", {"audioTrackFormatID": "AT_0001%04x_01" % (0x1001 + i),
"audioTrackFormatName": "PCM_" + BED_NAMES[i],
"formatDefinition": "PCM", "formatLabel": "0001"})
sub(tf, "audioStreamFormatIDRef", text="AS_0001%04x" % (0x1001 + i))
for i in range(N_OBJ):
tf = sub(af, "audioTrackFormat", {"audioTrackFormatID": "AT_0003%04x_01" % (0x1001 + i),
"audioTrackFormatName": "PCM_JOC_Object_%d" % (i + 1),
"formatDefinition": "PCM", "formatLabel": "0001"})
sub(tf, "audioStreamFormatIDRef", text="AS_0003%04x" % (0x1001 + i))
for i in range(10):
sf = sub(af, "audioStreamFormat", {"audioStreamFormatID": "AS_0001%04x" % (0x1001 + i),
"audioStreamFormatName": "PCM_" + BED_NAMES[i],
"formatDefinition": "PCM", "formatLabel": "0001"})
sub(sf, "audioChannelFormatIDRef", text="AC_0001%04x" % (0x1001 + i))
sub(sf, "audioPackFormatIDRef", text="AP_00011001")
sub(sf, "audioTrackFormatIDRef", text="AT_0001%04x_01" % (0x1001 + i))
for i in range(N_OBJ):
sf = sub(af, "audioStreamFormat", {"audioStreamFormatID": "AS_0003%04x" % (0x1001 + i),
"audioStreamFormatName": "PCM_JOC_Object_%d" % (i + 1),
"formatDefinition": "PCM", "formatLabel": "0001"})
sub(sf, "audioChannelFormatIDRef", text="AC_0003%04x" % (0x1001 + i))
sub(sf, "audioPackFormatIDRef", text="AP_0003%04x" % (0x1001 + i))
sub(sf, "audioTrackFormatIDRef", text="AT_0003%04x_01" % (0x1001 + i))
return ET.tostring(adm, encoding="utf-8", xml_declaration=True)
def build_chna():
out = bytearray()
out += struct.pack("<HH", 25, 25)
for i in range(10):
out += struct.pack("<H", i + 1)
out += ("ATU_%08x" % (i + 1)).encode()
out += ("AT_0001%04x_01" % (0x1001 + i)).encode()
out += b"AP_00011001" + b"\x00"
for i in range(N_OBJ):
out += struct.pack("<H", i + 11)
out += ("ATU_%08x" % (i + 11)).encode()
out += ("AT_0003%04x_01" % (0x1001 + i)).encode()
out += ("AP_0003%04x" % (0x1001 + i)).encode() + b"\x00"
return bytes(out)
def _checksum(seg):
s = len(seg)
for b in seg:
s += b
return (~s + 1) & 0xFF
def build_dbmd(object_count=25):
out = bytearray(struct.pack("<I", 0x01000006))
dd = bytearray(96)
dd[1] = 0x47
dd[5] = 0x60
dd[8] = 0x24; dd[9] = 0x24
out.append(7); out += struct.pack("<H", 96); out += bytes(dd)
out.append(_checksum(dd))
at = bytearray(248)
c0 = b"Created with EAC3JOC"; c1 = b"EAC3JOC Python Renderer"
at[0:len(c0)] = c0
at[32:32 + len(c1)] = c1
at[96], at[97], at[98] = 2, 1, 0
at[103] = 0x03
at[106] = 0x01
at[111] = 0x22; at[112] = 0xFF
out.append(9); out += struct.pack("<H", 248); out += bytes(at)
out.append(_checksum(at))
ob = bytearray(5 + 262 + object_count)
ob[0:4] = struct.pack("<I", 0xF8726FBD)
ob[4] = object_count
for i in range(5 + 262, len(ob)):
ob[i] = 0x84
out.append(10); out += struct.pack("<H", len(ob)); out += bytes(ob)
out.append(_checksum(ob))
out += b"\x00\x00"
return bytes(out)
class Sink25:
def __init__(self, path, channels, rate):
self.ch = channels; self.rate = rate; self.frames = 0
self.fp = open(path, "wb+")
self.fp.write(b"RF64" + struct.pack("<I", 0xFFFFFFFF) + b"WAVE")
self._chunk(b"ds64", b"\x00" * 64)
self._chunk(b"fmt ", self._fmt())
self._chunk(b"data", b"")
def _chunk(self, cid, body):
self.fp.write(cid + struct.pack("<I", len(body)) + body)
if len(body) & 1:
self.fp.write(b"\x00")
def _fmt(self):
return struct.pack("<HHIIHH", 1, self.ch, self.rate,
self.rate * self.ch * 3, self.ch * 3, 24)
def write_block(self, arr):
arr = arr.reshape(-1, self.ch)
i24 = (np.clip(arr, -1.0, 1.0) * 8388607.0).astype(np.int32)
self.fp.write(i24.view(np.uint8).reshape(-1, 4)[:, :3].tobytes())
self.frames += arr.shape[0]
def finalize(self, axml_bytes, chna_bytes, dbmd_bytes):
data_len = self.frames * self.ch * 3
self._chunk(b"axml", axml_bytes)
self._chunk(b"chna", chna_bytes)
self._chunk(b"dbmd", dbmd_bytes)
self.fp.seek(0, 2); total = self.fp.tell()
self.fp.seek(0); head = self.fp.read()
m = head.find(b"data")
if m >= 0:
self.fp.seek(m + 4); self.fp.write(struct.pack("<I", data_len))
m = head.find(b"ds64")
if m >= 0:
self.fp.seek(m + 8)
self.fp.write(struct.pack("<QQQI", total - 8, data_len, self.frames, 0))
self.fp.flush()
self.fp.close()
def build_master(out_path, bed_mm, obj_mm, kf_tracks, duration_sec, rate=48000,
block=480000):
n = min(bed_mm.shape[0], obj_mm.shape[0])
try:
from . import adm_serializer
except ImportError:
import adm_serializer
serial_axml = adm_serializer.build_axml
axml = serial_axml(kf_tracks, duration_sec)
chna = build_chna()
dbmd = build_dbmd(25)
sink = Sink25(out_path, 25, rate)
for st in range(0, n, block):
en = min(n, st + block)
blk = np.hstack((np.asarray(bed_mm[st:en], dtype=np.float32),
np.asarray(obj_mm[st:en, 1:16], dtype=np.float32)))
sink.write_block(blk)
sink.finalize(axml, chna, dbmd)
print(f"master25 -> {out_path} ({duration_sec:.2f}s, 25ch, axml={len(axml)}B, "
f"chna={len(chna)}B, dbmd={len(dbmd)}B)")
+136
View File
@@ -0,0 +1,136 @@
"""把 7.1.2 bed、15 个对象及其位置轨迹序列化为 ADM axml。
序列化结果采用固定元素顺序、属性顺序和十六进制 ADM 标识符,便于稳定输出和校验。
"""
BED_NAMES = ["RoomCentricLeft", "RoomCentricRight", "RoomCentricCenter", "RoomCentricLFE",
"RoomCentricLeftSideSurround", "RoomCentricRightSideSurround",
"RoomCentricLeftRearSurround", "RoomCentricRightRearSurround",
"RoomCentricLeftTopSurround", "RoomCentricRightTopSurround"]
BED_LABELS = ["RC_L", "RC_R", "RC_C", "RC_LFE", "RC_Lss", "RC_Rss",
"RC_Lrs", "RC_Rrs", "RC_Lts", "RC_Rts"]
BED_POS = [(-1.0, 1.0, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0), (-1.0, 1.0, -1.0),
(-1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (-1.0, -1.0, 0.0), (1.0, -1.0, 0.0),
(-1.0, 0.0, 1.0), (1.0, 0.0, 1.0)]
N_OBJ = 15
def ts(seconds):
s = int(seconds)
frac = int(round((seconds - s) * 100000))
if frac >= 100000:
s += 1; frac = 0
return f"{s // 3600:02d}:{s % 3600 // 60:02d}:{s % 60:02d}.{frac:05d}"
def esc(v):
return (str(v).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;"))
def build_axml(obj_tracks, duration_sec):
"""obj_tracks: [(name, [(rtime, x, y, z, dur), ...]) ×15]"""
w = []
a = w.append
a('<?xml version="1.0" encoding="utf-8"?>')
a('<ebuCoreMain xsi:schemaLocation="urn:ebu:metadata-schema:ebuCore_2016 ebucore.xsd" '
'lang="en" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
'xmlns="urn:ebu:metadata-schema:ebuCore_2016">')
a('<coreMetadata><format><audioFormatExtended>')
a(f'<audioProgramme audioProgrammeID="APR_1001" audioProgrammeName="EAC3JOC_Export" '
f'start="{ts(0)}" end="{ts(duration_sec)}">')
a('<audioContentIDRef>ACO_1001</audioContentIDRef>')
a('<audioContentIDRef>ACO_1002</audioContentIDRef>')
a('</audioProgramme>')
a('<audioContent audioContentID="ACO_1001" audioContentName="EAC3JOC_Master_Content">')
a('<audioObjectIDRef>AO_1001</audioObjectIDRef>')
a('<dialogue mixedContentKind="0">2</dialogue>')
a('</audioContent>')
a('<audioContent audioContentID="ACO_1002" audioContentName="Objects">')
for i in range(N_OBJ):
a(f'<audioObjectIDRef>AO_{0x100b + i:04x}</audioObjectIDRef>')
a('<dialogue mixedContentKind="0">2</dialogue>')
a('</audioContent>')
a(f'<audioObject audioObjectID="AO_1001" audioObjectName="Bed" '
f'start="{ts(0)}" duration="{ts(duration_sec)}">')
a('<audioPackFormatIDRef>AP_00011001</audioPackFormatIDRef>')
for i in range(10):
a(f'<audioTrackUIDRef>ATU_{i + 1:08x}</audioTrackUIDRef>')
a('</audioObject>')
for i in range(N_OBJ):
a(f'<audioObject audioObjectID="AO_{0x100b + i:04x}" audioObjectName="Audio Object {i+1}" '
f'start="{ts(0)}" duration="{ts(duration_sec)}">')
a(f'<audioPackFormatIDRef>AP_0003{0x1001 + i:04x}</audioPackFormatIDRef>')
a(f'<audioTrackUIDRef>ATU_{11 + i:08x}</audioTrackUIDRef>')
a('</audioObject>')
a('<audioPackFormat audioPackFormatID="AP_00011001" audioPackFormatName="EAC3JOCBedPack" '
'typeDefinition="DirectSpeakers" typeLabel="0001">')
for i in range(10):
a(f'<audioChannelFormatIDRef>AC_0001{0x1001 + i:04x}</audioChannelFormatIDRef>')
a('</audioPackFormat>')
for i in range(N_OBJ):
a(f'<audioPackFormat audioPackFormatID="AP_0003{0x1001 + i:04x}" '
f'audioPackFormatName="JOC_Object_{i+1}" typeDefinition="Objects" typeLabel="0003">')
a(f'<audioChannelFormatIDRef>AC_0003{0x1001 + i:04x}</audioChannelFormatIDRef>')
a('</audioPackFormat>')
for i in range(10):
a(f'<audioChannelFormat audioChannelFormatID="AC_0001{0x1001 + i:04x}" '
f'audioChannelFormatName="{BED_NAMES[i]}" typeDefinition="DirectSpeakers" typeLabel="0001">')
a(f'<audioBlockFormat audioBlockFormatID="AB_0001{0x1001 + i:04x}_00000001">')
a('<cartesian>1</cartesian>')
x, y, z = BED_POS[i]
a(f'<position coordinate="X">{x:.10f}</position>')
a(f'<position coordinate="Y">{y:.10f}</position>')
if z != 0:
a(f'<position coordinate="Z">{z:.10f}</position>')
a(f'<speakerLabel>{BED_LABELS[i]}</speakerLabel>')
a('</audioBlockFormat>')
a('</audioChannelFormat>')
for i, (oname, kfs) in enumerate(obj_tracks):
a(f'<audioChannelFormat audioChannelFormatID="AC_0003{0x1001 + i:04x}" '
f'audioChannelFormatName="{oname}" typeDefinition="Objects" typeLabel="0003">')
for k, keyframe in enumerate(kfs):
t, x, y, z, dur = keyframe[:5]
interpolation = keyframe[5] if len(keyframe) > 5 else 0.0
a(f'<audioBlockFormat audioBlockFormatID="AB_0003{0x1001 + i:04x}_{k + 1:08x}" '
f'rtime="{ts(t)}" duration="{ts(dur)}">')
a('<cartesian>1</cartesian>')
a(f'<position coordinate="X">{x:.10f}</position>')
a(f'<position coordinate="Y">{y:.10f}</position>')
if z != 0:
a(f'<position coordinate="Z">{z:.10f}</position>')
a(f'<jumpPosition interpolationLength="{interpolation:.5f}">1</jumpPosition>')
a('</audioBlockFormat>')
a('</audioChannelFormat>')
for i in range(10):
a(f'<audioTrackUID UID="ATU_{i + 1:08x}" bitDepth="24" sampleRate="48000">')
a(f'<audioTrackFormatIDRef>AT_0001{0x1001 + i:04x}_01</audioTrackFormatIDRef>')
a('<audioPackFormatIDRef>AP_00011001</audioPackFormatIDRef>')
a('</audioTrackUID>')
for i in range(N_OBJ):
a(f'<audioTrackUID UID="ATU_{11 + i:08x}" bitDepth="24" sampleRate="48000">')
a(f'<audioTrackFormatIDRef>AT_0003{0x1001 + i:04x}_01</audioTrackFormatIDRef>')
a(f'<audioPackFormatIDRef>AP_0003{0x1001 + i:04x}</audioPackFormatIDRef>')
a('</audioTrackUID>')
for i in range(10):
a(f'<audioTrackFormat audioTrackFormatID="AT_0001{0x1001 + i:04x}_01" '
f'audioTrackFormatName="PCM_{BED_NAMES[i]}" formatDefinition="PCM" formatLabel="0001">')
a(f'<audioStreamFormatIDRef>AS_0001{0x1001 + i:04x}</audioStreamFormatIDRef>')
a('</audioTrackFormat>')
for i in range(N_OBJ):
a(f'<audioTrackFormat audioTrackFormatID="AT_0003{0x1001 + i:04x}_01" '
f'audioTrackFormatName="PCM_JOC_Object_{i+1}" formatDefinition="PCM" formatLabel="0001">')
a(f'<audioStreamFormatIDRef>AS_0003{0x1001 + i:04x}</audioStreamFormatIDRef>')
a('</audioTrackFormat>')
for i in range(10):
a(f'<audioStreamFormat audioStreamFormatID="AS_0001{0x1001 + i:04x}" '
f'audioStreamFormatName="PCM_{BED_NAMES[i]}" formatDefinition="PCM" formatLabel="0001">')
a(f'<audioChannelFormatIDRef>AC_0001{0x1001 + i:04x}</audioChannelFormatIDRef>')
a('<audioPackFormatIDRef>AP_00011001</audioPackFormatIDRef>')
a(f'<audioTrackFormatIDRef>AT_0001{0x1001 + i:04x}_01</audioTrackFormatIDRef>')
a('</audioStreamFormat>')
for i in range(N_OBJ):
a(f'<audioStreamFormat audioStreamFormatID="AS_0003{0x1001 + i:04x}" '
f'audioStreamFormatName="PCM_JOC_Object_{i+1}" formatDefinition="PCM" formatLabel="0001">')
a(f'<audioChannelFormatIDRef>AC_0003{0x1001 + i:04x}</audioChannelFormatIDRef>')
a(f'<audioPackFormatIDRef>AP_0003{0x1001 + i:04x}</audioPackFormatIDRef>')
a(f'<audioTrackFormatIDRef>AT_0003{0x1001 + i:04x}_01</audioTrackFormatIDRef>')
a('</audioStreamFormat>')
a('</audioFormatExtended></format></coreMetadata>')
a('</ebuCoreMain>')
return ''.join(w).encode('utf-8')
+199
View File
@@ -0,0 +1,199 @@
"""校验 ADM BWF 的 RF64、通道、axml、chna、dbmd 和对象引用结构。
用法:``python src/adm_validate.py <file.wav> [more.wav ...]``,全部通过时退出码为 0。
"""
import struct, sys, re, os
def fail(msgs, m): msgs.append(m)
def walk_chunks(path):
chunks, ds64 = [], {}
with open(path, "rb") as f:
riff = f.read(4); f.read(4); wave = f.read(4)
if riff not in (b"RIFF", b"RF64"):
return None, None, f"File does not have a 'RIFF' or 'RF64' chunk"
if wave != b"WAVE":
return None, None, "File does not have a required 'WAVE' chunk"
while True:
off = f.tell()
cid = f.read(4)
if len(cid) < 4: break
sz = struct.unpack("<I", f.read(4))[0]
if cid == b"ds64":
body = f.read(sz + (sz & 1))
riff64, data64, sample64, _ = struct.unpack("<QQQI", body[:28])
ds64 = dict(riff64=riff64, data64=data64, sample64=sample64)
chunks.append(("ds64", off, sz)); continue
chunks.append((cid.decode("latin1"), off, sz))
eff = ds64.get("data64", sz) if (sz == 0xFFFFFFFF and cid == b"data") else sz
f.seek(off + 8 + eff + (eff & 1))
return chunks, ds64, None
def read_body(path, chunks, cid):
for c, off, sz in chunks:
if c == cid:
with open(path, "rb") as f:
f.seek(off + 8)
return f.read(sz)
return None
def parse_chna(body):
n_track, n_uid = struct.unpack("<HH", body[:4])
rows, p = [], 4
while p + 40 <= len(body):
trk = struct.unpack("<H", body[p:p+2])[0]
uid = body[p+2:p+14].rstrip(b"\x00").decode()
tf = body[p+14:p+28].rstrip(b"\x00").decode()
pk = body[p+28:p+40].rstrip(b"\x00").decode()
rows.append((trk, uid, tf, pk)); p += 40
return n_track, n_uid, rows
def decode_channel_input(ao_id):
"""将 ``AO_xxxx`` 的十六进制标识符解码为低 12 位通道输入号。"""
m = re.fullmatch(r"AO_([0-9a-fA-F]+)", ao_id)
if not m:
return None
v = int(m.group(1), 16)
if v > 0x1FFF: # 超过 12 位通道域
return None
return v & 0x0FFF
def ts_sec(s):
h, m, rest = s.split(":")
return int(h) * 3600 + int(m) * 60 + float(rest)
def validate(path, axml_override=None, chna_override=None):
msgs = []
chunks, ds64, err = walk_chunks(path)
if err:
return [err]
have = {c for c, _, _ in chunks}
for need in ("fmt ", "data", "axml", "chna", "dbmd"):
if need not in have:
fail(msgs, f"File does not have a required '{need.strip()}' chunk")
if msgs:
return msgs
fmt = read_body(path, chunks, "fmt ")
f_tag, f_ch, f_rate, _, _, f_bits = struct.unpack("<HHIIHH", fmt[:16])
chna_body = chna_override if chna_override is not None else read_body(path, chunks, "chna")
n_track, n_uid, rows = parse_chna(chna_body)
if f_ch != n_track:
fail(msgs, f"Mismatched number of audio channels and chna entries "
f"(fmt={f_ch} chna={n_track})")
ax_raw = axml_override if axml_override is not None else read_body(path, chunks, "axml")
ax = ax_raw.decode("utf-8")
# --- audioObjectID 十六进制通道输入号解码 ---
objs = re.findall(r'audioObjectID="(AO_[0-9a-zA-Z]+)"', ax)
bed_ch, obj_ch = [], []
for ao in objs:
cid = decode_channel_input(ao)
if cid is None:
fail(msgs, f"Invalid ADM BWF XML format: cannot decode channel "
f"input ID from AudioObjectID '{ao}'")
continue
if ao == "AO_1001":
bed_ch.append(cid)
else:
if cid <= 10:
fail(msgs, f"Source channel index should be greater than 10 "
f"for objects ('{ao}' -> {cid})")
obj_ch.append(cid)
# UID 十六进制 → 必须与 chna 表一致
uid_map = {uid: trk for trk, uid, tf, pk in rows}
for uid in re.findall(r'UID="(ATU_[0-9a-zA-Z]+)"', ax):
if uid not in uid_map:
fail(msgs, f"'{uid}' is not referenced in 'chna' chunk UID table")
continue
m = re.fullmatch(r"ATU_([0-9a-fA-F]+)", uid)
if m:
v = int(m.group(1), 16)
if v > 128:
fail(msgs, f"Channel index out of range (UID {uid} -> {v})")
# 轨数一致性:axml audioTrackUID 数 == fmt 声道数
n_tu = len(re.findall(r"<audioTrackUID ", ax))
if n_tu != f_ch:
fail(msgs, f"Number of channels declared in ADM ({n_tu}) does not "
f"match 'fmt ' chunk ({f_ch})")
# sampleRate / bitDepth 一致
for sr in set(re.findall(r'sampleRate="(\d+)"', ax)):
if int(sr) != f_rate:
fail(msgs, f"Mismatched track sample rate between ADM and WAV ({sr} vs {f_rate})")
for bd in set(re.findall(r'bitDepth="(\d+)"', ax)):
if int(bd) != f_bits:
fail(msgs, f"Mismatched track bit depth between ADM and WAV ({bd} vs {f_bits})")
# audioProgramme 唯一性 / audioContent ≥1
if ax.count("<audioProgramme ") != 1:
fail(msgs, "ADM has more than one audioProgramme object -- there must be only one"
if ax.count("<audioProgramme ") > 1
else "ADM does not have a required audioProgramme object")
if "<audioContent " not in ax:
fail(msgs, "audioProgramme object does not have a required audioContent object")
# 每个 channelFormat ≥1 blockFormat + 对象块链连续性
cfs = re.findall(r'<audioChannelFormat [^>]*typeLabel="0003".*?</audioChannelFormat>', ax, re.S)
object_block_formats = 0
for seg in cfs:
cf_id = re.search(r'audioChannelFormatID="([^"]+)"', seg).group(1)
block_xml = re.findall(r'<audioBlockFormat [^>]*rtime="[^"]+".*?</audioBlockFormat>',
seg, re.S)
object_block_formats += len(block_xml)
blocks = []
for block_index, block in enumerate(block_xml, 1):
timing = re.search(r'rtime="([^"]+)" duration="([^"]+)"', block)
if timing is None:
continue
rtime, duration = timing.groups()
blocks.append((rtime, duration))
jump = re.search(
r'<jumpPosition interpolationLength="([^"]+)">1</jumpPosition>', block)
if jump is not None and float(jump.group(1)) > ts_sec(duration) + 1e-8:
fail(msgs, f"Interpolation length exceeds duration in block format "
f"{block_index} of {cf_id}: {jump.group(1)} > {duration}")
if not blocks:
fail(msgs, f"AudioChannelFormat {cf_id} is missing audioBlockFormat sub-element")
continue
for i in range(len(blocks) - 1):
end_i = ts_sec(blocks[i][0]) + ts_sec(blocks[i][1])
nxt = ts_sec(blocks[i + 1][0])
if abs(end_i - nxt) > 2e-5:
fail(msgs, f"Time gap between block format {i+1} and {i+2} of {cf_id}: "
f"{end_i:.5f} vs {nxt:.5f}")
return msgs, dict(fmt_ch=f_ch, fmt_rate=f_rate, fmt_bits=f_bits,
chna=n_track, objects=len(obj_ch), bed=len(bed_ch),
trackUIDs=n_tu, axml_bytes=len(ax_raw),
audioBlockFormats=ax.count("<audioBlockFormat "),
objectBlockFormats=object_block_formats)
def main():
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("files", nargs="+")
ap.add_argument("--axml-file", default=None, help="用该文件内容替换 wav 内 axml(对照实验)")
ap.add_argument("--chna-file", default=None, help="用该文件内容替换 wav 内 chna(对照实验)")
a = ap.parse_args()
ax_o = open(a.axml_file, "rb").read() if a.axml_file else None
ch_o = open(a.chna_file, "rb").read() if a.chna_file else None
rc = 0
for p in a.files:
r = validate(p, axml_override=ax_o, chna_override=ch_o)
name = os.path.basename(p)
if isinstance(r, list):
msgs, info = r, {}
else:
msgs, info = r
if msgs:
rc = 1
print(f"[FAIL] {name}")
for m in msgs:
print(" -", m)
else:
print(f"[PASS] {name} {info}")
return rc
if __name__ == "__main__":
sys.exit(main())
+282
View File
@@ -0,0 +1,282 @@
"""从常见 E-AC-3 同步帧直接提取连续 EMDF 容器。
扫描器检查八种全局位对齐,定位 ``0x5838`` 同步字,验证容器长度并解析各
payload config,因此不要求 EMDF 在原始 E-AC-3 文件中按字节对齐。
本模块有意只覆盖“完整 EMDF 容器在一个同步帧中连续出现”的常见情形。不解析
E-AC-3 mantissa,也不重组被音频数据隔开的多个 skip-field 碎片;遇到这种输入会
明确报错,让上层决定是否使用兼容桥。
"""
from dataclasses import dataclass
import csv
import hashlib
from pathlib import Path
import numpy as np
from variant_error import UnsupportedVariantError
SYNCWORD = 0x5838
REQUIRED_JOC_IDS = frozenset((11, 14))
class EmdfError(ValueError):
"""EMDF 或其 E-AC-3 传输结构不符合本实现支持的范围。"""
class BitReader:
"""MSB-first 位读取器;位置以源数据的绝对 bit offset 表示。"""
def __init__(self, data, position=0, limit=None):
self.data = memoryview(data)
self.position = int(position)
self.limit = len(self.data) * 8 if limit is None else int(limit)
def read(self, count):
count = int(count)
if count < 0 or self.position + count > self.limit:
raise EmdfError(f"位流越界 @bit{self.position}, need={count}, limit={self.limit}")
value = 0
while count:
byte_pos = self.position >> 3
removed_left = self.position & 7
take = min(count, 8 - removed_left)
shift = 8 - removed_left - take
value = (value << take) | ((self.data[byte_pos] >> shift) & ((1 << take) - 1))
self.position += take
count -= take
return value
def skip(self, count):
self.read(count)
def read_bytes(self, count):
return bytes(self.read(8) for _ in range(count))
def variable_bits(reader, width, max_groups=8):
"""读取 EMDF ``variable_bits(width)`` 变长整数。"""
value = 0
for _ in range(max_groups):
value += reader.read(width)
more = reader.read(1)
if not more:
return value
value = (value + 1) << width
raise EmdfError(f"variable_bits({width}) 延伸组过多")
@dataclass(frozen=True)
class EmdfContainer:
start_bit: int
raw: bytes
payloads: dict
sample_offsets: dict
def _parse_at(data, start_bit):
"""在已知 syncword 的 bit offset 解析一个 EMDF 容器。"""
reader = BitReader(data, start_bit)
if reader.read(16) != SYNCWORD:
raise EmdfError(f"EMDF syncword 不匹配 @bit{start_bit}")
length = reader.read(16)
body_start = reader.position
body_end = body_start + length * 8
if body_end > reader.limit:
raise EmdfError(f"EMDF 容器越界 @bit{start_bit}: length={length}")
reader.limit = body_end
version = reader.read(2)
if version == 3:
version += variable_bits(reader, 2)
key_id = reader.read(3)
if key_id == 7:
key_id += variable_bits(reader, 3)
# TS 103 420 JOC 使用 version=0/key_id=0;严格限制也能排除音频中的伪 marker。
if version != 0 or key_id != 0:
raise EmdfError(f"不支持的 EMDF version/key_id: {version}/{key_id}")
payloads = {}
sample_offsets = {}
terminated = False
while reader.position + 5 <= body_end:
payload_id = reader.read(5)
if payload_id == 0:
terminated = True
break
if payload_id == 0x1F:
payload_id += variable_bits(reader, 5)
if payload_id in payloads:
raise EmdfError(f"同一 EMDF 容器重复 payload id {payload_id}")
has_sample_offset = bool(reader.read(1))
sample_offset = (reader.read(12) >> 1) if has_sample_offset else 0
if reader.read(1):
variable_bits(reader, 11) # duration
if reader.read(1):
variable_bits(reader, 2) # group id
if reader.read(1):
reader.skip(8) # codec data
if not reader.read(1): # discard_unknown_payload
frame_aligned = False
if not has_sample_offset:
frame_aligned = bool(reader.read(1))
if frame_aligned:
reader.skip(2)
if has_sample_offset or frame_aligned:
reader.skip(7)
payload_size = variable_bits(reader, 8)
if reader.position + payload_size * 8 > body_end:
raise EmdfError(
f"payload id {payload_id} 越界: size={payload_size}, @bit{reader.position}")
payloads[payload_id] = reader.read_bytes(payload_size)
sample_offsets[payload_id] = sample_offset
if not terminated:
raise EmdfError("EMDF 容器缺少 payload id 0 终止符")
total_bytes = 4 + length
raw_reader = BitReader(data, start_bit, start_bit + total_bytes * 8)
raw = raw_reader.read_bytes(total_bytes)
return EmdfContainer(start_bit, raw, payloads, sample_offsets)
def _marker_offsets(data):
"""以 NumPy 批量检查八种位移,返回可能的 0x5838 bit offsets。"""
source = np.frombuffer(data, dtype=np.uint8)
if source.size < 4:
return []
offsets = []
for shift in range(8):
if shift == 0:
aligned = source
else:
aligned = np.bitwise_or(
np.left_shift(source[:-1].astype(np.uint16), shift) & 0xFF,
np.right_shift(source[1:].astype(np.uint16), 8 - shift),
).astype(np.uint8)
hits = np.flatnonzero((aligned[:-1] == 0x58) & (aligned[1:] == 0x38))
offsets.extend(int(hit) * 8 + shift for hit in hits)
return sorted(offsets)
def find_joc_emdf(frame):
"""返回同步帧中唯一、连续且包含 ID11/ID14 的 JOC EMDF 容器。"""
matches = []
offsets = _marker_offsets(frame)
parsed_candidates = []
parse_errors = []
for start_bit in offsets:
try:
container = _parse_at(frame, start_bit)
except EmdfError as exc:
if len(parse_errors) < 8:
parse_errors.append({"start_bit": start_bit, "error": str(exc)})
continue
parsed_candidates.append({
"start_bit": start_bit,
"payload_ids": list(container.payloads),
"payload_lengths": {str(k): len(v) for k, v in container.payloads.items()},
})
if REQUIRED_JOC_IDS.issubset(container.payloads):
matches.append(container)
if not matches:
raise UnsupportedVariantError(
"emdf_transport", "no_contiguous_joc_container",
"同步帧中未找到可连续解析且同时包含 ID11/ID14 的 EMDF 容器",
details={
"syncframe_bytes": len(frame),
"marker_bit_offsets": offsets,
"parsed_candidates": parsed_candidates,
"candidate_parse_errors": parse_errors,
"repair_hint": "检查 EMDF 是否跨多个 audio-block skip field 分片,或 payload config 是否变化",
})
if len(matches) != 1:
starts = [item.start_bit for item in matches]
raise UnsupportedVariantError(
"emdf_transport", "multiple_joc_containers",
"同步帧中存在多个可用 JOC EMDF,当前无法自动选择",
details={"syncframe_bytes": len(frame), "joc_container_start_bits": starts})
return matches[0]
def parse_container(data):
"""解析从 syncword 开始、已经重新按字节对齐保存的 EMDF 容器。"""
container = _parse_at(data, 0)
if len(container.raw) != len(data):
raise EmdfError(f"EMDF 文件尾有额外数据: parsed={len(container.raw)}, file={len(data)}")
return container
def iter_eac3_frames(data):
"""按 E-AC-3 ``frmsiz`` 遍历同步帧,拒绝静默重同步。"""
pos = 0
while pos < len(data):
if pos + 4 > len(data) or data[pos:pos + 2] != b"\x0b\x77":
raise UnsupportedVariantError(
"eac3_transport", "syncframe_header",
"E-AC-3 同步帧头无效或出现了未处理的子流排列",
details={
"byte_offset": pos,
"remaining_bytes": len(data) - pos,
"next_16_bytes_hex": data[pos:pos + 16].hex(),
})
size = ((((data[pos + 2] & 7) << 8) | data[pos + 3]) + 1) * 2
if pos + size > len(data):
raise UnsupportedVariantError(
"eac3_transport", "truncated_syncframe",
"E-AC-3 末帧长度超过输入剩余数据",
details={
"byte_offset": pos,
"declared_frame_bytes": size,
"remaining_bytes": len(data) - pos,
})
yield data[pos:pos + size]
pos += size
def extract_index(eac3_path, output_dir, max_frames=None):
"""将裸 E-AC-3 的连续 EMDF 保存为 ``frames.csv + emdf/``。"""
output_dir = Path(output_dir)
emdf_dir = output_dir / "emdf"
emdf_dir.mkdir(parents=True, exist_ok=True)
frames = iter_eac3_frames(Path(eac3_path).read_bytes())
rows = []
for frame_number, frame in enumerate(frames):
if max_frames is not None and frame_number >= max_frames:
break
try:
container = find_joc_emdf(frame)
except UnsupportedVariantError as exc:
exc.add_context(frame=frame_number, details={"syncframe_bytes": len(frame)})
raise
except EmdfError as exc:
raise UnsupportedVariantError(
"emdf_transport", "container_syntax",
"EMDF 容器语法无法解析",
frame=frame_number,
details={"syncframe_bytes": len(frame), "parser_error": str(exc)}) from exc
digest = hashlib.sha256(container.raw).hexdigest()
target = emdf_dir / f"{digest}.bin"
if not target.is_file():
target.write_bytes(container.raw)
rows.append({
"frame": frame_number,
"emdf_hash": digest,
"emdf_size": len(container.raw),
"emdf_start_bit": container.start_bit,
"payload_ids": ";".join(str(x) for x in container.payloads),
"error": "",
})
if (frame_number + 1) % 1000 == 0:
print(f"[metadata] {frame_number + 1} frames", flush=True)
if not rows:
raise EmdfError("E-AC-3 输入中没有可处理的同步帧")
with (output_dir / "frames.csv").open("w", encoding="utf-8", newline="") as fp:
fields = ("frame", "emdf_hash", "emdf_size", "emdf_start_bit", "payload_ids", "error")
writer = csv.DictWriter(fp, fieldnames=fields)
writer.writeheader()
writer.writerows(rows)
return output_dir
+76
View File
@@ -0,0 +1,76 @@
"""解包 EVO MD-set evolution 载荷,返回各 payload ID 的字节数据和位偏移。"""
_MARK = "1001001000000"
# 各子载荷字段: (id, 头部前缀, 同步标记, 后缀常量, 尺寸域位数, 是否有转义)
# 头部前缀 = 5 位 id 的 MSB 二进制(id11 字段前另有 5 位容器前导 00000)
# 尺寸域单位 = nibble(4 位)。id14 有转义:9 位值=0 → 再读 9 位 = 字节数。
_LAYOUT = [
(11, "0000001011", "010000000000000", 8, False),
(14, "01110", "01000000000000", 9, True),
(2, "00010", "000100", 7, False),
(1, "00001", "1110000000000000000000000000", 4, False),
(30, "11110", "1110000000000000000000000000", 4, False),
]
def _msb_bits(data: bytes):
return [(x >> (7 - i)) & 1 for x in data for i in range(8)]
def _val(bits, off, n):
v = 0
for b in bits[off:off + n]:
v = (v << 1) | b
return v
class _LooseSkip(Exception):
def __init__(self, ident):
self.ident = ident
def unpack_evolution(payload: bytes, loose=False):
"""解包 evolution 载荷 → (subs, offsets)。subs 键为 id 整数。
loose=True 时对每个 id 的 (前缀+标记+后缀) 全模式做位流重同步扫描
(不同编码流的子载荷次序/内部常量可有合法差异,如 kanata 的 id11)。"""
bits = _msb_bits(payload)
pos = 0
subs = {}
offsets = {}
for ident, pref, suff, sbits, escape in _LAYOUT:
pat = pref + _MARK + suff
if loose:
hit = -1
for i in range(pos, len(bits) - len(pat)):
if ''.join(map(str, bits[i:i + len(pat)])) == pat:
hit = i
break
if hit < 0:
continue
pos = hit + len(pat)
else:
for name, const, expect in (("前缀", bits[pos:pos + len(pref)], pref),
("标记", bits[pos + len(pref):pos + len(pref) + len(_MARK)], _MARK),
("后缀", bits[pos + len(pref) + len(_MARK):
pos + len(pref) + len(_MARK) + len(suff)], suff)):
got = ''.join(map(str, const))
if got != expect:
raise ValueError(f"id={ident}: {name}常量不匹配 @bit{pos} got={got} want={expect}")
pos += len(pref) + len(_MARK) + len(suff)
n_nib = _val(bits, pos, sbits)
pos += sbits
if escape and n_nib == 1:
n_nib = 512 + _val(bits, pos, sbits)
pos += sbits
n_bits = n_nib * 4
body = bits[pos:pos + n_bits]
pos += n_bits
b = bytearray(len(body) // 8)
for i in range(0, len(body) // 8 * 8, 8):
v = 0
for x in body[i:i + 8]:
v = (v << 1) | x
b[i // 8] = v
subs[ident] = bytes(b)
offsets[ident] = pos
return subs, offsets
+176
View File
@@ -0,0 +1,176 @@
"""解析 JOC 位流并生成对象混合矩阵。
范围:
- EMDF ID14 的 joc_header、joc_info 和 Huffman joc_data;
- 差分解码得到 joc_mix_mtx_q;
- 去量化得到 joc_mix_mtx_dq;
- 位流自洽验证(joc_data 后剩余 = padding_bits 0..7 + 可能 joc_ext_data)
后续的时间插值、QMF/时域重建和 ``joc_clipgain`` 位于 ``renderer.py``。
Sparse 分支仍缺少实际样本验证。
"""
from pathlib import Path
import numpy as np
# 格式:节点数组 [left, right];正 = 内部节点索引,负 = 叶(值 = -node-1)
_TABLES_PATH = Path(__file__).resolve().parent.parent / "data" / "tables.npz"
_HUFF_NAMES = (
"joc_huff_code_coarse_generic",
"joc_huff_code_fine_generic",
"joc_huff_code_coarse_coeff_sparse",
"joc_huff_code_fine_coeff_sparse",
"joc_huff_code_5ch_pos_index_sparse",
"joc_huff_code_7ch_pos_index_sparse",
)
def _load_huff_tables():
with np.load(_TABLES_PATH) as tables:
return {
name: np.asarray(tables[name], dtype=np.int64).tolist()
for name in _HUFF_NAMES
}
H = _load_huff_tables()
JOC_NUM_CHANNELS = {0: 5, 1: 7, 2: 7, 3: 5, 4: 7} # Table 33
JOC_NUM_BANDS = {0: 1, 1: 3, 2: 5, 3: 7, 4: 9, 5: 12, 6: 15, 7: 23} # Table 35
_PUBLIC_TABLE39_EXCERPT_UNUSED = { # 仅保留作表格差异说明;渲染映射在 joc_qmf.py。
23: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22],
}
class BR:
def __init__(self, data, pos=0):
self.d = data
self.p = pos
def bits(self, n):
v = 0
for _ in range(n):
v = (v << 1) | ((self.d[self.p >> 3] >> (7 - (self.p & 7))) & 1)
self.p += 1
return v
def huff_decode(tree, br):
node = 0
while node >= 0:
b = br.bits(1)
node = tree[node][b]
return -node - 1
def get_huff_code(mode, typ, nch):
if typ == "IDX":
return H["joc_huff_code_5ch_pos_index_sparse" if nch == 5 else "joc_huff_code_7ch_pos_index_sparse"]
if typ == "VEC":
return H["joc_huff_code_coarse_coeff_sparse" if mode == 0 else "joc_huff_code_fine_coeff_sparse"]
# MTX
return H["joc_huff_code_coarse_generic" if mode == 0 else "joc_huff_code_fine_generic"]
def parse_joc(payload):
"""解析 id14 载荷(joc() 位流)。返回字段 dict + 解析后剩余位数。"""
br = BR(payload)
out = {}
out["dmx_config_idx"] = br.bits(3)
out["num_objects_bits"] = br.bits(6)
out["ext_config_idx"] = br.bits(3)
n_objects = out["num_objects_bits"] + 1
n_channels = JOC_NUM_CHANNELS.get(out["dmx_config_idx"])
out["n_objects"], out["n_channels"] = n_objects, n_channels
out["clipgain_x_bits"] = br.bits(3)
out["clipgain_y_bits"] = br.bits(5)
out["seq_count_bits"] = br.bits(10)
# clipgain = 1 + (y/32)·2^(x−4),值域为 [1, 8.75]。
out["clipgain"] = 1 + out["clipgain_y_bits"] / 32.0 * 2 ** (out["clipgain_x_bits"] - 4)
objs = []
for obj in range(n_objects):
o = {}
o["present"] = br.bits(1)
if o["present"]:
o["num_bands_idx"] = br.bits(3)
o["n_bands"] = JOC_NUM_BANDS[o["num_bands_idx"]]
o["sparse"] = br.bits(1)
o["quant_idx"] = br.bits(1)
o["slope_idx"] = br.bits(1)
o["num_dpoints_bits"] = br.bits(1)
o["n_dpoints"] = o["num_dpoints_bits"] + 1
if o["slope_idx"] == 1:
o["offset_ts"] = [br.bits(5) + 1 for _ in range(o["n_dpoints"])]
objs.append(o)
out["objs"] = objs
# joc_data(Huffman)
for obj, o in enumerate(objs):
if not o["present"]:
continue
nquant = 96 if o["quant_idx"] == 0 else 192
o["channel_idx"] = []
o["vec"] = []
o["mtx"] = []
for dp in range(o["n_dpoints"]):
if o["sparse"] == 1:
# Sparse JOC 使用 VEC/IDX Huffman 树;此分支尚无真实码流验证。
ci0 = br.bits(3)
tree = get_huff_code(n_channels, "IDX", n_channels)
ci = [ci0] + [huff_decode(tree, br) for _ in range(o["n_bands"] - 1)]
o["channel_idx"].append(ci)
tree = get_huff_code(o["quant_idx"], "VEC", n_channels)
vec = [huff_decode(tree, br) for _ in range(o["n_bands"])]
o["vec"].append(vec)
else:
tree = get_huff_code(o["quant_idx"], "MTX", n_channels)
mtx = [[huff_decode(tree, br) for _ in range(o["n_bands"])]
for _ in range(n_channels)]
o["mtx"].append(mtx)
out["data_end_bits"] = br.p
out["remaining_bits"] = len(payload) * 8 - br.p
out["tail_bytes"] = payload[br.p // 8:]
return out
def diff_decode(out):
"""6.6.2:差分解码 → joc_mix_mtx_q[obj][dp][ch][pb]。"""
mix_q = {}
n_ch = out["n_channels"]
for obj, o in enumerate(out["objs"]):
if not o["present"]:
continue
nquant = 96 if o["quant_idx"] == 0 else 192
q = np.zeros((o["n_dpoints"], n_ch, o["n_bands"]), dtype=np.int64)
for dp in range(o["n_dpoints"]):
if o["sparse"] == 1:
# Sparse 差分路径尚无真实码流验证。
offset = 50 if o["quant_idx"] == 0 else 100
ci = o["channel_idx"][dp]
vec = o["vec"][dp]
for pb in range(o["n_bands"]):
ci_mod = ci[0] if pb == 0 else (ci[pb - 1] + ci[pb]) % n_ch
for ch in range(n_ch):
if ch == ci_mod:
if pb == 0:
q[dp][ch][pb] = (offset + vec[pb]) % nquant
else:
q[dp][ch][pb] = (q[dp][ch][pb - 1] + vec[pb]) % nquant
else:
q[dp][ch][pb] = offset
else:
offset = 48 if o["quant_idx"] == 0 else 96
mtx = o["mtx"][dp]
for ch in range(n_ch):
q[dp][ch][0] = (offset + mtx[ch][0]) % nquant
for pb in range(1, o["n_bands"]):
q[dp][ch][pb] = (q[dp][ch][pb - 1] + mtx[ch][pb]) % nquant
mix_q[obj] = q
return mix_q
def dequantize(out, mix_q):
"""6.6.4:去量化 → joc_mix_mtx_dq。"""
mix_dq = {}
for obj, o in enumerate(out["objs"]):
if not o["present"]:
continue
nquant = 96 if o["quant_idx"] == 0 else 192
q = mix_q[obj]
dq = (q.astype(np.float64) - nquant / 2) * 820 / (4096 * (1 + o["quant_idx"]))
mix_dq[obj] = dq
return mix_dq
+151
View File
@@ -0,0 +1,151 @@
"""实现 JOC QMF、参数带映射和矩阵时间插值。
静态表保存在 ``data`` 中;NumPy 批量函数保持各通道、各对象的状态彼此独立。
"""
from pathlib import Path
import numpy as np
N = 64
_TABLES = np.load(Path(__file__).resolve().parent.parent / "data" / "tables.npz")
ANALYSIS_WINDOW = np.asarray(_TABLES["analysis_window"], dtype=np.float64)
QMF5_WINDOW = np.asarray(_TABLES["qmf5_window"], dtype=np.float64)
# 子带到参数带的映射表。
_TABLE39 = {
23: "0 1 2 3 4 5 6 7 8 9 10 11 12 12 13 13 14 14 15 15 16 16 16 17 17 17 18 18 18 18 19 19 19 19 19 20 20 20 20 20 20 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22",
15: "0 1 2 3 4 5 6 7 8 9 9 10 10 11 11 11 12 12 12 12 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14",
12: "0 1 2 3 4 4 5 5 6 6 6 7 7 7 8 8 8 8 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11",
9: "0 1 2 3 3 3 4 5 5 6 6 6 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8",
7: "0 1 2 2 3 3 3 3 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6",
5: "0 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4",
3: "0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2",
1: "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
}
_PB_MAP = {k: np.fromstring(v, dtype=np.int64, sep=" ") for k, v in _TABLE39.items()}
def sb_to_pb_table(n_bands):
try:
return _PB_MAP[n_bands]
except KeyError as exc:
raise ValueError(f"不支持的 JOC 参数带数: {n_bands}") from exc
def interp_matrix(obj_info, dq, prev, n_ts=24):
"""按数据点和跨帧状态插值,返回 ``[channel,subband,timeslot]``。"""
dq_sb = np.asarray(dq, dtype=np.float64)[:, :, sb_to_pb_table(dq.shape[2])]
previous = np.asarray(prev, dtype=np.float64)
n_dp = obj_info["n_dpoints"]
slope = obj_info["slope_idx"]
if slope == 0:
if n_dp == 1:
alpha = (np.arange(n_ts, dtype=np.float64) + 1.0) / n_ts
return previous[:, :, None] * (1.0 - alpha) + dq_sb[0, :, :, None] * alpha
half = n_ts // 2
a0 = (np.arange(half, dtype=np.float64) + 1.0) / half
a1 = (np.arange(n_ts - half, dtype=np.float64) + 1.0) / (n_ts - half)
first = previous[:, :, None] * (1.0 - a0) + dq_sb[0, :, :, None] * a0
second = dq_sb[0, :, :, None] * (1.0 - a1) + dq_sb[1, :, :, None] * a1
return np.concatenate((first, second), axis=2)
ts = np.arange(n_ts)
offsets = obj_info.get("offset_ts", [])
if n_dp == 1:
return np.where(ts[None, None, :] < offsets[0], previous[:, :, None], dq_sb[0, :, :, None])
out = np.where(ts[None, None, :] < offsets[0], previous[:, :, None], dq_sb[0, :, :, None])
return np.where(ts[None, None, :] < offsets[1], out, dq_sb[1, :, :, None])
def qmf_analysis_step(fifo, ring):
"""分析 QMF 的单时隙入口;批量入口见 :func:`qmf_analysis_frame`。"""
f = np.asarray(fifo, dtype=np.float64)
r = np.asarray(ring, dtype=np.float64)
single = f.ndim == 2
if single:
f, r = f[None, ...], r[None, ...]
x, new = qmf_analysis_frame(f, r[:, None, :])
interleaved = np.empty((len(f), 128), dtype=np.float64)
interleaved[:, 0::2] = x[:, :, 0].real
interleaved[:, 1::2] = x[:, :, 0].imag
return (interleaved[0], new[0]) if single else (interleaved, new)
def qmf_analysis_frame(fifo, rings):
"""批量完成整帧时隙的分析窗、旋转和 64 点 FFT。"""
f = np.asarray(fifo, dtype=np.float64)
r = np.asarray(rings, dtype=np.float64)
if f.ndim != 3 or f.shape[1:] != (9, 64) or r.ndim != 3 or r.shape[0] != f.shape[0] or r.shape[2] != 64:
raise ValueError(f"analysis QMF shape 错误: fifo={f.shape}, rings={r.shape}")
slots = r.shape[1]
seq = np.concatenate((f[:, ::-1, :], r), axis=1)
windows = np.lib.stride_tricks.sliding_window_view(seq, 9, axis=1)
history = windows[:, :slots].transpose(0, 1, 3, 2)[:, :, ::-1, :]
t = ANALYSIS_WINDOW
v36 = np.sum(history[:, :, 0::2, :] * t[[8, 6, 4, 2, 0]][None, None], axis=2)
v40 = np.sum(history[:, :, 1::2, :] * t[[7, 5, 3, 1]][None, None], axis=2) + r * t[9]
pairs = np.stack((v40, v36), axis=-1)
kernel = pairs.reshape(len(f), slots, 16, 4, 2)[:, :, ::-1, ::-1, :].reshape(len(f), slots, 128)
k = np.arange(64, dtype=np.float64)
a = 0.5 * np.sin(np.pi * k / 128.0)
b = 0.5 * np.cos(np.pi * k / 128.0)
re, im = kernel[:, :, 0::2], kernel[:, :, 1::2]
freq = np.fft.fft((im * a - re * b) + 1j * (im * b + re * a), axis=2) / 64.0
src = np.empty((len(f), slots, 128), dtype=np.float64)
src[:, :, 0::2], src[:, :, 1::2] = freq.real, freq.imag
out = np.empty_like(src).reshape(len(f), slots, 32, 4)
out[:, :, :, 0] = src[:, :, 0:64:2]
out[:, :, :, 1] = -src[:, :, 1:64:2]
out[:, :, :, 2] = src[:, :, 126:62:-2]
out[:, :, :, 3] = src[:, :, 127:63:-2]
flat = out.reshape(len(f), slots, 128)
complex_qmf = (flat[:, :, 0::2] + 1j * flat[:, :, 1::2]).transpose(0, 2, 1)
combined = np.concatenate((f[:, ::-1, :], r), axis=1)
new_fifo = combined[:, -9:, :][:, ::-1, :].copy()
return complex_qmf, new_fifo
_SURROUND_DC_A = np.array([
-.0006242550443857908, -.0019234686624258757, -.0042654648423194885,
-.008168308064341545, -.014327201060950756, -.023759860545396805,
-.03757232800126076, -.05577569454908371, -.07568276673555374,
-.09172472357749939, -.5979374051094055, -.09172472357749939,
-.07568276673555374, -.05577569454908371, -.03757232800126076,
-.023759860545396805, -.014327201060950756, -.008168308064341545,
-.0042654648423194885, -.0019234686624258757, -.0006242550443857908,
], dtype=np.float64)
_SURROUND_DC_B = np.array([
.0013996040215715766, .003839150769636035, .007512642536312342,
.012419373728334904, .018367428332567215, .0249701626598835,
.03167900815606117, .03785000368952751, .04283412545919418,
.04607561603188515, .047200120985507965, .04607561603188515,
.04283412545919418, .03785000368952751, .03167900815606117,
.0249701626598835, .018367428332567215, .012419373728334904,
.007512642536312342, .003839150769636035, .0013996040215715766,
], dtype=np.float64)
_SURROUND_DC_C = _SURROUND_DC_B + 1j * _SURROUND_DC_A
def surround_post_frame(x, delay, dc_hist):
"""处理 Ls/Rs 的 10 槽延迟、-j 旋转和 band-0 FIR。"""
src = np.asarray(x, dtype=np.complex128)
qdelay = np.asarray(delay, dtype=np.complex128).copy()
hist = np.asarray(dc_hist, dtype=np.complex128).copy()
if src.shape != (2, 64, 24):
raise ValueError(f"surround QMF shape 错误: {src.shape}")
out = np.empty_like(src)
for group in range(0, 24, 4):
current = src[:, :, group:group + 4].transpose(0, 2, 1)
queued = np.concatenate((qdelay, current), axis=1)
block = -1j * queued[:, :4, :]
qdelay = queued[:, 4:, :]
dc_buf = np.concatenate((hist, current[:, :, 0]), axis=1)
windows = np.lib.stride_tricks.sliding_window_view(dc_buf, 21, axis=1)
block[:, :, 0] = 2.0 * np.sum(windows * _SURROUND_DC_C[None, None, :], axis=2)
hist = dc_buf[:, 4:]
out[:, :, group:group + 4] = block.transpose(0, 2, 1)
return out, qdelay, hist
+324
View File
@@ -0,0 +1,324 @@
"""Evolution sidecar 的 JOC/OAMD 纯 Python 解析、校验和可读输出。"""
from collections import Counter
import csv
import hashlib
import json
from pathlib import Path
from adm_atmos import q_to_adm_xyz
from evo_unpack import unpack_evolution
from emdf import EmdfError, find_joc_emdf, iter_eac3_frames, parse_container
from joc_decode import parse_joc
from oamd_bits import JocFieldState, frame_update_values
from variant_error import UnsupportedVariantError, bytes_descriptor
class DirectPayloadIndex:
"""One-pass in-memory view of contiguous EMDF containers in an E-AC-3 file.
The optional cache directory keeps the existing ``frames.csv + emdf/``
contract, but normal processing reuses the containers already parsed during
the scan instead of reading and parsing thousands of small files again.
"""
def __init__(self, rows, subpayloads, sample_offsets, directory=None):
self.rows = rows
self._subpayloads = subpayloads
self._sample_offsets = sample_offsets
self.directory = Path(directory) if directory is not None else None
@classmethod
def from_eac3(cls, eac3_path, max_frames=None, cache_dir=None):
cache_dir = Path(cache_dir) if cache_dir is not None else None
emdf_dir = None
if cache_dir is not None:
emdf_dir = cache_dir / "emdf"
emdf_dir.mkdir(parents=True, exist_ok=True)
rows = []
payloads = []
offsets = []
frames = iter_eac3_frames(Path(eac3_path).read_bytes())
for frame_number, frame in enumerate(frames):
if max_frames is not None and frame_number >= max_frames:
break
try:
container = find_joc_emdf(frame)
except UnsupportedVariantError as exc:
exc.add_context(frame=frame_number, details={"syncframe_bytes": len(frame)})
raise
except EmdfError as exc:
raise UnsupportedVariantError(
"emdf_transport", "container_syntax",
"EMDF 容器语法无法解析",
frame=frame_number,
details={"syncframe_bytes": len(frame), "parser_error": str(exc)}) from exc
digest = hashlib.sha256(container.raw).hexdigest()
if emdf_dir is not None:
target = emdf_dir / f"{digest}.bin"
if not target.is_file():
target.write_bytes(container.raw)
rows.append({
"frame": frame_number,
"emdf_hash": digest,
"emdf_size": len(container.raw),
"emdf_start_bit": container.start_bit,
"payload_ids": ";".join(str(x) for x in container.payloads),
"error": "",
})
payloads.append(container.payloads)
offsets.append(container.sample_offsets)
if (frame_number + 1) % 1000 == 0:
print(f"[metadata] {frame_number + 1} frames", flush=True)
if not rows:
raise EmdfError("E-AC-3 输入中没有可处理的同步帧")
if cache_dir is not None:
with (cache_dir / "frames.csv").open("w", encoding="utf-8", newline="") as fp:
fields = ("frame", "emdf_hash", "emdf_size", "emdf_start_bit", "payload_ids", "error")
writer = csv.DictWriter(fp, fieldnames=fields)
writer.writeheader()
writer.writerows(rows)
return cls(rows, payloads, offsets, cache_dir)
def subpayloads(self, row):
return self._subpayloads[int(row["frame"])]
def subpayload_sample_offset(self, row, payload_id):
return int(self._sample_offsets[int(row["frame"])].get(payload_id, 0))
def __len__(self):
return len(self.rows)
class PayloadIndex:
"""旧 evolution sidecar 或直接 EMDF sidecar 的严格顺序视图。"""
def __init__(self, directory):
self.directory = Path(directory)
csv_path = self.directory / "frames.csv"
self.payload_dir = self.directory / "payloads"
self.emdf_dir = self.directory / "emdf"
if not csv_path.is_file() or not (self.payload_dir.is_dir() or self.emdf_dir.is_dir()):
raise FileNotFoundError(
f"元数据目录需要 frames.csv 和 payloads/ 或 emdf/: {self.directory}")
with csv_path.open(encoding="utf-8", newline="") as fp:
self.rows = list(csv.DictReader(fp))
if not self.rows:
raise ValueError("frames.csv 为空")
self._sub_cache = {}
self._sample_offset_cache = {}
def payload(self, row):
payload_hash = row.get("payload_hash", "")
if not payload_hash:
raise ValueError(f"frame {row.get('frame', '?')} 缺少 evolution payload")
path = self.payload_dir / f"{payload_hash}.bin"
if not path.is_file():
raise FileNotFoundError(path)
return path.read_bytes()
def subpayloads(self, row):
"""返回 ``{payload_id: bytes}``,屏蔽两种 sidecar 容器的差异。"""
emdf_hash = row.get("emdf_hash", "")
if emdf_hash:
key = ("emdf", emdf_hash)
if key not in self._sub_cache:
path = self.emdf_dir / f"{emdf_hash}.bin"
if not path.is_file():
raise FileNotFoundError(path)
container = parse_container(path.read_bytes())
self._sub_cache[key] = container.payloads
self._sample_offset_cache[key] = container.sample_offsets
return self._sub_cache[key]
payload_hash = row.get("payload_hash", "")
key = ("evolution", payload_hash)
if key not in self._sub_cache:
self._sub_cache[key], _ = unpack_evolution(self.payload(row), loose=True)
self._sample_offset_cache[key] = {}
return self._sub_cache[key]
def subpayload_sample_offset(self, row, payload_id):
"""返回 EMDF payload 的外层 sample offset;旧 sidecar 没有该字段时为 0。"""
self.subpayloads(row)
emdf_hash = row.get("emdf_hash", "")
key = (("emdf", emdf_hash) if emdf_hash else
("evolution", row.get("payload_hash", "")))
return int(self._sample_offset_cache.get(key, {}).get(payload_id, 0))
def __len__(self):
return len(self.rows)
def _frame_record(frame, parsed, state):
present = [i for i, obj in enumerate(parsed["objs"]) if obj["present"]]
sparse = [i for i in present if parsed["objs"][i]["sparse"]]
bands = Counter(parsed["objs"][i]["n_bands"] for i in present)
positions = []
for obj in range(1, 16):
q1, q2, q3 = (state.q[(obj, key)] for key in ("q1", "q2", "q3"))
x, y, z = q_to_adm_xyz(q1, q2, q3)
positions.append([round(float(x), 7), round(float(y), 7), round(float(z), 7)])
return {
"frame": frame,
"sequence": parsed["seq_count_bits"],
"downmix_config": parsed["dmx_config_idx"],
"extension_config": parsed["ext_config_idx"],
"objects_declared": parsed["n_objects"],
"objects_present": present,
"sparse_objects": sparse,
"parameter_bands": {str(k): v for k, v in sorted(bands.items())},
"clipgain": parsed["clipgain"],
"oamd_xyz": positions,
}
def inspect(index, limit=None, print_frames=False):
"""逐帧完整解析 ID11/ID14,并返回可 JSON 序列化的汇总。"""
rows = index.rows if limit is None else index.rows[:limit]
oamd = JocFieldState()
frame_records = []
configs = Counter()
active = Counter()
band_totals = Counter()
sparse_counts = Counter()
clipgains = []
for seq, row in enumerate(rows):
frame_number = int(row.get("frame", seq))
try:
subs = index.subpayloads(row)
except UnsupportedVariantError as exc:
exc.add_context(frame=frame_number)
raise
except Exception as exc:
raise UnsupportedVariantError(
"metadata_container", "sidecar_or_emdf_parse",
"元数据容器无法解析",
frame=frame_number,
details={"exception_type": type(exc).__name__, "parser_error": str(exc)}) from exc
if 11 not in subs or 14 not in subs:
raise UnsupportedVariantError(
"emdf_payloads", "missing_required_payload",
"EMDF 缺少 ID11/OAMD 或 ID14/JOC",
frame=frame_number,
details={
"payload_ids": list(subs),
"payload_lengths": {str(k): len(v) for k, v in subs.items()},
})
try:
oamd.apply(frame_update_values(subs[11]))
except UnsupportedVariantError as exc:
exc.add_context(frame=frame_number, details={"payload_id": 11})
raise
except Exception as exc:
raise UnsupportedVariantError(
"oamd", "payload_syntax",
"OAMD 字段解析失败",
frame=frame_number,
details={
"payload_id": 11,
"payload": bytes_descriptor(subs[11]),
"exception_type": type(exc).__name__,
"parser_error": str(exc),
}) from exc
try:
parsed = parse_joc(subs[14])
except UnsupportedVariantError as exc:
exc.add_context(frame=frame_number, details={"payload_id": 14})
raise
except Exception as exc:
payload = subs[14]
header = {}
if len(payload) >= 4:
value = int.from_bytes(payload[:4], "big")
header = {
"downmix_config": (value >> 29) & 7,
"objects_minus_one": (value >> 23) & 63,
"extension_config": (value >> 20) & 7,
}
raise UnsupportedVariantError(
"joc", "payload_syntax",
"JOC ID14 解析失败",
frame=frame_number,
details={
"payload_id": 14,
"header_probe": header,
"payload": bytes_descriptor(payload),
"exception_type": type(exc).__name__,
"parser_error": str(exc),
"repair_hint": "检查 JOC header、对象数、参数带、Huffman 或扩展字段",
}) from exc
sparse = [i for i, obj in enumerate(parsed["objs"]) if obj["present"] and obj["sparse"]]
if sparse:
raise UnsupportedVariantError(
"joc", "sparse_joc",
"发现尚未验证的 Sparse JOC 帧",
frame=frame_number,
details={
"sparse_objects": sparse,
"downmix_config": parsed["dmx_config_idx"],
"extension_config": parsed["ext_config_idx"],
"objects": parsed["n_objects"],
"payload": bytes_descriptor(subs[14]),
"repair_hint": "需要 Sparse JOC 实际样本及对应输出建立回归后再启用",
})
if parsed["n_channels"] != 5 or parsed["n_objects"] > 15:
raise UnsupportedVariantError(
"joc", "unsupported_configuration",
"JOC 核心通道数或对象数超出当前渲染器范围",
frame=frame_number,
details={
"downmix_config": parsed["dmx_config_idx"],
"core_channels": parsed["n_channels"],
"objects": parsed["n_objects"],
"extension_config": parsed["ext_config_idx"],
"payload": bytes_descriptor(subs[14]),
})
rec = _frame_record(frame_number, parsed, oamd)
configs[(parsed["dmx_config_idx"], parsed["ext_config_idx"],
parsed["n_channels"], parsed["n_objects"])] += 1
active[len(rec["objects_present"])] += 1
band_totals.update({int(k): v for k, v in rec["parameter_bands"].items()})
sparse_counts[len(rec["sparse_objects"])] += 1
clipgains.append(float(parsed["clipgain"]))
if print_frames:
print(json.dumps(rec, ensure_ascii=False, separators=(",", ":")))
frame_records.append(rec)
summary = {
"frames": len(rows),
"frame_samples": 1536,
"sample_rate": 48000,
"duration_sec": len(rows) * 1536 / 48000,
"configurations": [
{"downmix_config": k[0], "extension_config": k[1], "core_channels": k[2],
"objects": k[3], "frames": count}
for k, count in sorted(configs.items())
],
"active_object_count_histogram": {str(k): v for k, v in sorted(active.items())},
"parameter_band_totals": {str(k): v for k, v in sorted(band_totals.items())},
"sparse_object_count_histogram": {str(k): v for k, v in sorted(sparse_counts.items())},
"clipgain_min": min(clipgains),
"clipgain_max": max(clipgains),
"first_frame": frame_records[0],
"last_frame": frame_records[-1],
}
emdf_rows = [row for row in rows if row.get("emdf_start_bit", "") != ""]
if emdf_rows:
starts = [int(row["emdf_start_bit"]) for row in emdf_rows]
id_sets = Counter(row.get("payload_ids", "") for row in emdf_rows)
summary["emdf_transport"] = {
"continuous_containers": len(emdf_rows),
"start_bit_min": min(starts),
"start_bit_max": max(starts),
"bit_alignment_histogram": {
str(k): v for k, v in sorted(Counter(x & 7 for x in starts).items())
},
"payload_id_order_histogram": dict(sorted(id_sets.items())),
}
return summary
def write_summary(index, output, limit=None, print_frames=False):
summary = inspect(index, limit=limit, print_frames=print_frames)
output = Path(output)
output.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
return summary
+276
View File
@@ -0,0 +1,276 @@
"""ctypes bridge for the dependency-free MSVC C++ JOC DSP core.
Metadata parsing intentionally stays in Python. One C call consumes a complete
1536-sample frame, so Python is not involved in the hot 24-timeslot x 15-object
DSP loops.
"""
from __future__ import annotations
import ctypes
import os
from pathlib import Path
import sys
import numpy as np
from evo_unpack import unpack_evolution
from joc_decode import dequantize, diff_decode, parse_joc
FRAME_SAMPLES = 1536
MAX_OBJECTS = 15
MAX_DPOINTS = 2
CORE_CHANNELS = 5
MAX_BANDS = 23
ABI_VERSION = 1
class NativeBackendUnavailable(RuntimeError):
pass
def native_library_filename():
if sys.platform == "win32":
return "eac3joc_core.dll"
if sys.platform == "darwin":
return "libeac3joc_core.dylib"
if sys.platform.startswith("linux"):
return "libeac3joc_core.so"
raise NativeBackendUnavailable(f"unsupported native platform: {sys.platform}")
def _candidate_libraries():
override = os.environ.get("EAC3JOC_NATIVE_LIBRARY")
if override:
yield Path(override).expanduser()
root = Path(__file__).resolve().parent.parent
yield root / "lib" / native_library_filename()
def find_native_library(explicit=None):
if explicit is not None:
path = Path(explicit).expanduser().resolve()
if not path.is_file():
raise NativeBackendUnavailable(f"native library not found: {path}")
return path
checked = []
for item in _candidate_libraries():
path = item.resolve()
checked.append(str(path))
if path.is_file():
return path
raise NativeBackendUnavailable("native library not found; checked: " + "; ".join(checked))
def default_native_threads():
override = os.environ.get("EAC3JOC_NATIVE_THREADS")
if override is not None:
value = int(override)
if value < 1:
raise ValueError("EAC3JOC_NATIVE_THREADS must be at least 1")
return min(value, MAX_OBJECTS)
return 2 if (os.cpu_count() or 1) >= 4 else 1
def _load_library(path):
lib = ctypes.CDLL(str(path))
float_p = ctypes.POINTER(ctypes.c_float)
u8_p = ctypes.POINTER(ctypes.c_uint8)
double_p = ctypes.POINTER(ctypes.c_double)
lib.ejoc_abi_version.argtypes = []
lib.ejoc_abi_version.restype = ctypes.c_uint32
lib.ejoc_build_info.argtypes = []
lib.ejoc_build_info.restype = ctypes.c_char_p
lib.ejoc_renderer_create.argtypes = []
lib.ejoc_renderer_create.restype = ctypes.c_void_p
lib.ejoc_renderer_destroy.argtypes = [ctypes.c_void_p]
lib.ejoc_renderer_destroy.restype = None
lib.ejoc_renderer_reset.argtypes = [ctypes.c_void_p]
lib.ejoc_renderer_reset.restype = ctypes.c_int
lib.ejoc_renderer_set_threads.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
lib.ejoc_renderer_set_threads.restype = ctypes.c_int
lib.ejoc_renderer_thread_count.argtypes = [ctypes.c_void_p]
lib.ejoc_renderer_thread_count.restype = ctypes.c_uint32
lib.ejoc_renderer_last_error.argtypes = [ctypes.c_void_p]
lib.ejoc_renderer_last_error.restype = ctypes.c_char_p
lib.ejoc_renderer_process.argtypes = [
ctypes.c_void_p,
float_p,
float_p,
ctypes.c_uint32,
u8_p,
u8_p,
u8_p,
u8_p,
double_p,
ctypes.c_double,
ctypes.c_float,
ctypes.c_float,
float_p,
]
lib.ejoc_renderer_process.restype = ctypes.c_int
abi = int(lib.ejoc_abi_version())
if abi != ABI_VERSION:
raise NativeBackendUnavailable(f"native ABI mismatch: library={abi}, Python={ABI_VERSION}")
return lib
class NativeJocRenderer:
"""Stateful whole-frame native DSP renderer with the Python renderer API shape."""
def __init__(self, output_scale=1.0, library_path=None, threads=None):
self.output_scale = np.float32(output_scale)
if not np.isfinite(self.output_scale):
raise ValueError("output_scale must be finite")
self.library_path = find_native_library(library_path)
self._lib = _load_library(self.library_path)
self._handle = self._lib.ejoc_renderer_create()
if not self._handle:
raise MemoryError("ejoc_renderer_create failed")
requested_threads = default_native_threads() if threads is None else int(threads)
if requested_threads < 1:
raise ValueError("threads must be at least 1")
result = self._lib.ejoc_renderer_set_threads(self._handle, requested_threads)
if result:
self._raise_native("set_threads", result)
self.threads = int(self._lib.ejoc_renderer_thread_count(self._handle))
self._n_bands = np.zeros(MAX_OBJECTS, dtype=np.uint8)
self._n_dpoints = np.zeros(MAX_OBJECTS, dtype=np.uint8)
self._slope_idx = np.zeros(MAX_OBJECTS, dtype=np.uint8)
self._offset_ts = np.zeros((MAX_OBJECTS, MAX_DPOINTS), dtype=np.uint8)
self._dq = np.zeros(
(MAX_OBJECTS, MAX_DPOINTS, CORE_CHANNELS, MAX_BANDS),
dtype=np.float64,
)
self._output = np.zeros((16, FRAME_SAMPLES), dtype=np.float32)
@property
def build_info(self):
value = self._lib.ejoc_build_info()
return value.decode("utf-8", "replace") if value else ""
@staticmethod
def decode_payload(payload_bytes):
subs, _ = unpack_evolution(payload_bytes, loose=True)
return NativeJocRenderer.decode_subpayloads(subs)
@staticmethod
def decode_subpayloads(subs):
if 14 not in subs:
raise ValueError("EMDF missing ID14/JOC")
out = parse_joc(subs[14])
mix_q = diff_decode(out)
mix_dq = dequantize(out, mix_q)
return out, mix_q, mix_dq
def reset(self):
self._require_open()
result = self._lib.ejoc_renderer_reset(self._handle)
if result:
self._raise_native("reset", result)
def close(self):
handle = getattr(self, "_handle", None)
if handle:
self._lib.ejoc_renderer_destroy(handle)
self._handle = None
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
self.close()
def __del__(self):
try:
self.close()
except Exception:
pass
def _require_open(self):
if not self._handle:
raise RuntimeError("native renderer is closed")
def _raise_native(self, operation, code):
raw = self._lib.ejoc_renderer_last_error(self._handle)
detail = raw.decode("utf-8", "replace") if raw else "unknown native error"
raise RuntimeError(f"native {operation} failed ({code}): {detail}")
def _pack_frame(self, out, mix_dq):
if out["n_channels"] != CORE_CHANNELS:
raise ValueError(f"native core requires 5 JOC channels, got {out['n_channels']}")
if out["n_objects"] > MAX_OBJECTS:
raise ValueError(f"native core supports at most 15 objects, got {out['n_objects']}")
self._n_bands.fill(0)
self._n_dpoints.fill(0)
self._slope_idx.fill(0)
self._offset_ts.fill(0)
mask = 0
for object_index, info in enumerate(out["objs"]):
if not info["present"]:
continue
if info["sparse"]:
raise ValueError("native core does not accept unvalidated Sparse JOC")
bands = int(info["n_bands"])
points = int(info["n_dpoints"])
if bands > MAX_BANDS or points > MAX_DPOINTS:
raise ValueError(f"native descriptor out of range: bands={bands}, points={points}")
values = np.asarray(mix_dq[object_index], dtype=np.float64)
expected = (points, CORE_CHANNELS, bands)
if values.shape != expected:
raise ValueError(f"object {object_index} dq shape {values.shape}, expected {expected}")
mask |= 1 << object_index
self._n_bands[object_index] = bands
self._n_dpoints[object_index] = points
self._slope_idx[object_index] = int(info["slope_idx"])
offsets = info.get("offset_ts", ())
self._offset_ts[object_index, :len(offsets)] = offsets
self._dq[object_index, :points, :, :bands] = values
return mask
def render_frame(self, payload_bytes, bed5_pcm, lfe_pcm=None):
subs, _ = unpack_evolution(payload_bytes, loose=True)
return self.render_subpayloads(subs, bed5_pcm, lfe_pcm)
def render_subpayloads(self, subs, bed5_pcm, lfe_pcm=None):
self._require_open()
out, _, mix_dq = self.decode_subpayloads(subs)
object_mask = self._pack_frame(out, mix_dq)
bed5 = np.ascontiguousarray(bed5_pcm, dtype=np.float32)
if bed5.shape != (CORE_CHANNELS, FRAME_SAMPLES):
raise ValueError(f"core PCM shape must be (5,1536), got {bed5.shape}")
if lfe_pcm is None:
lfe = None
lfe_ptr = ctypes.POINTER(ctypes.c_float)()
else:
lfe = np.ascontiguousarray(lfe_pcm, dtype=np.float32)
if lfe.shape != (FRAME_SAMPLES,):
raise ValueError(f"LFE shape must be (1536,), got {lfe.shape}")
lfe_ptr = lfe.ctypes.data_as(ctypes.POINTER(ctypes.c_float))
result = self._lib.ejoc_renderer_process(
self._handle,
bed5.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
lfe_ptr,
object_mask,
self._n_bands.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
self._n_dpoints.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
self._slope_idx.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
self._offset_ts.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),
self._dq.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
float(out["clipgain"]),
ctypes.c_float(0.0625),
ctypes.c_float(self.output_scale),
self._output.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
)
if result:
self._raise_native("process", result)
return self._output, None
def native_available(library_path=None):
try:
path = find_native_library(library_path)
lib = _load_library(path)
return True, str(path), (lib.ejoc_build_info() or b"").decode("utf-8", "replace")
except Exception as exc:
return False, None, str(exc)
+216
View File
@@ -0,0 +1,216 @@
"""OAMD 位载荷 → 16 个对象槽的 q1/q2/q3 增量状态。
槽 0 是 bed/LFE;槽 1..15 对应输出 ch1..15 的对象元数据。
"""
import numpy as np
from variant_error import UnsupportedVariantError, bytes_descriptor
Q1_OFF = 192
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,
)
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)):
src = np.frombuffer(bits_one, dtype=np.uint8)
else:
src = np.asarray(bits_one, dtype=np.uint8)
if src.ndim == 1 and src.shape[0] in (536, 552):
bits = src
elif src.size in (67, 69):
bits = np.unpackbits(src.reshape(-1), bitorder="big")
else:
raw = (bytes(bits_one) if isinstance(bits_one, (bytes, bytearray, memoryview))
else np.asarray(bits_one, dtype=np.uint8).tobytes())
raise UnsupportedVariantError(
"oamd", f"payload_length_{len(raw)}B",
f"发现未覆盖的 OAMD 载荷长度 {len(raw)}B",
details={
"supported_payload_bytes": [67, 69],
"payload": bytes_descriptor(raw),
"repair_hint": "检查 OAMD header、element 数量及可选字段造成的位偏移变化",
})
raw_payload = np.packbits(bits, bitorder="big").tobytes()
return bits, raw_payload
class _BitReader:
def __init__(self, bits):
self.bits = bits
self.position = 0
def read(self, count):
end = self.position + count
if end > len(self.bits):
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(reader, width, max_groups=5):
value = 0
for _ in range(max_groups + 1):
value += reader.read(width)
if not reader.read(1):
return value
value = (value + 1) << width
raise ValueError(f"OAMD variable_bits({width}) 延伸组过多")
def _update_timing(bits, alternate_object_present, element_count, raw_payload):
"""按 OAMD element/MDUpdateInfo 读取位置块的开始偏移和 ramp 时长。"""
reader = _BitReader(bits)
reader.position = 14
for _ in range(element_count):
element_index = reader.read(4)
element_length = _variable_bits(reader, 4)
element_end = reader.position + element_length + 1
reader.skip(5 if alternate_object_present else 1)
if element_index == 1:
offset_code = reader.read(2)
if offset_code == 0:
sample_offset = 0
elif offset_code == 1:
sample_offset = SAMPLE_OFFSET_INDEX[reader.read(2)]
elif offset_code == 2:
sample_offset = reader.read(5)
else:
raise UnsupportedVariantError(
"oamd", "md_sample_offset_mode",
"OAMD 使用了当前未覆盖的 MD sample-offset 模式",
details={"payload": bytes_descriptor(raw_payload)})
block_count = reader.read(3) + 1
blocks = []
for _block in range(block_count):
block_offset_factor = reader.read(6)
block_offset = sample_offset + block_offset_factor * 32
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, ramp_duration))
if len(blocks) != 1:
raise UnsupportedVariantError(
"oamd", "multiple_position_blocks",
"OAMD 一帧含多个对象位置更新块,固定位置窗口不能安全套用",
details={
"block_count": len(blocks),
"blocks": blocks,
"payload": bytes_descriptor(raw_payload),
"repair_hint": "按 ObjectInfoBlock 顺序逐块解析坐标,再生成分段 ADM ramp",
})
return blocks[0]
reader.position = element_end
raise UnsupportedVariantError(
"oamd", "missing_object_element",
"OAMD 中没有 object element (element_index=1)",
details={"payload": bytes_descriptor(raw_payload)})
def frame_update(bits_one):
"""单帧 OAMD → 位置字段增量及其 sample offset/ramp duration。"""
bits, raw_payload = _payload_bits(bits_one)
header = {
"version": int((bits[0] << 1) | bits[1]),
"objects_minus_one": int(sum(int(bits[2 + i]) << (4 - i) for i in range(5))),
"dynamic_object_only": int(bits[7]),
"lfe_present": int(bits[8]),
"alternate_object_present": int(bits[9]),
"element_count": int(sum(int(bits[10 + i]) << (3 - i) for i in range(4))),
}
if raw_payload[:2] != b"\x1f\x88":
raise UnsupportedVariantError(
"oamd", f"header_signature_{raw_payload[:2].hex()}",
"OAMD 长度已知,但 header 与当前位置字段布局不一致",
details={
"supported_header_prefix_hex": "1f88",
"header_probe": header,
"payload": bytes_descriptor(raw_payload),
"repair_hint": "按新 header 的 program assignment 和 element 布局重新定位对象位置字段",
})
block_offset, ramp_duration = _update_timing(
bits, bool(header["alternate_object_present"]), header["element_count"], raw_payload)
weights = 1 << np.arange(7, -1, -1)
out = {}
for obj in range(16):
start = 112 + 31 * (obj - 3)
wq1 = int((bits[start:start + 8] * weights).sum())
wq2 = int((bits[start + 8:start + 16] * weights).sum())
wq3 = int((bits[start + 16:start + 24] * weights).sum())
if obj and (wq1 >> 6 != 3 or wq2 & 2 != 2 or wq3 & 0x1F != 1):
raise UnsupportedVariantError(
"oamd", "position_layout_signature",
"OAMD 长度和 header 已知,但对象位置字段标记或位偏移发生变化",
details={
"object_slot": obj,
"position_start_bit": start,
"q1_window_hex": f"{wq1:02x}",
"q2_window_hex": f"{wq2:02x}",
"q3_window_hex": f"{wq3:02x}",
"header_probe": header,
"payload": bytes_descriptor(raw_payload),
"repair_hint": "解析 OAMD element 可选字段并更新每个对象的位置窗口偏移",
})
k1 = wq1 - Q1_OFF
if obj == 0:
out[(0, "q1")] = None
out[(0, "q2")] = None
else:
out[(obj, "q1")] = q_of(k1, N_Q12) if 0 <= k1 <= N_Q12 else None
k2 = wq2 >> 2
if obj != 0:
out[(obj, "q2")] = q_of(k2, N_Q12) if 0 <= k2 <= N_Q12 else None
k3 = (wq2 & 1) * 8 + (wq3 >> 5)
out[(obj, "q3")] = q_of(k3, N_Q3) if 0 <= k3 <= N_Q3 else None
return {
"values": out,
"block_offset_samples": block_offset,
"ramp_duration_samples": ramp_duration,
}
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)
+227
View File
@@ -0,0 +1,227 @@
"""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
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_quantum_samples,
object_delay_samples, object_index):
if not events:
return [(0.0, 0.0, 0.0, 0.0, total_samples / float(rate), 0.0)]
# 初始位置从成品 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 = coded_start + object_delay_samples
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)
effective_ramp = max(0, int(ramp_samples) - update_quantum_samples)
if effective_ramp == 0:
_append_point(points, start, target, 0)
current = target
continue
end = start + math.ceil(effective_ramp / update_quantum_samples) * update_quantum_samples
if event_index + 1 < len(events):
next_start = events[event_index + 1][0] + object_delay_samples
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": "按 64-sample 状态机截断旧 ramp,再从当前插值位置启动新 ramp",
})
# 逐位置更新节拍复现状态机。1536-sample ramp 在首次 64-sample
# 更新后剩余 1472 samples,因此共有 23 个中间/终点坐标。
future = effective_ramp
elapsed = 0
position = current
while future > 0:
amount = min(update_quantum_samples / float(future), 1.0)
position = _lerp_xyz(position, target, amount)
elapsed += update_quantum_samples
sample = start + elapsed
if sample >= total_samples:
break
_append_point(points, sample, position, update_quantum_samples)
future -= update_quantum_samples
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_quantum_samples,
object_delay_samples, object_index):
"""Represent each linear OAMD ramp with one ADM interpolation block.
The existing dense64 representation keeps the old position at ``start``,
writes its first interpolated target at ``start + quantum``, and lets ADM
interpolate that block over one quantum. Consequently, the interpreted
motion begins at ``start + quantum`` and reaches the final target at
``start + ramp_duration``. This compact form preserves that timing with one
target block whose interpolationLength is ``ramp_duration - quantum``.
"""
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)
for event_index, (coded_start, target, ramp_samples) in enumerate(events[1:], 1):
event_start = coded_start + object_delay_samples
if event_start >= total_samples:
break
effective_ramp = max(0, int(ramp_samples) - update_quantum_samples)
block_start = event_start + (update_quantum_samples if effective_ramp else 0)
if block_start >= total_samples:
break
ramp_end = block_start + effective_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 = next_coded_start + object_delay_samples
next_effective = max(0, int(next_ramp_samples) - update_quantum_samples)
next_block_start = next_event_start + (update_quantum_samples if next_effective else 0)
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 = effective_ramp
available = total_samples - block_start
if effective_ramp > available:
block_target = _lerp_xyz(current, target, available / float(effective_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_quantum_samples,
object_delay_samples, object_index, trajectory_mode):
if trajectory_mode == "compact":
return _compact_events(events, total_samples, rate, update_quantum_samples,
object_delay_samples, object_index)
if trajectory_mode == "dense64":
return _expand_events_dense64(events, total_samples, rate, update_quantum_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_quantum_samples=64, object_delay_samples=640,
trajectory_mode="compact"):
"""从统一 metadata index 构造 15 条 ADM 轨迹。
返回 ``[(name, [(rtime,x,y,z,duration,interpolation), ...]), ...]``。
OAMD 的内外层 sample offset、block offset 和 ramp 均保留。
``trajectory_mode="compact"`` 用一个长 ADM interpolation block 表示每条
线性 ramp;``dense64`` 保留逐 64-sample 展开作为兼容回退。
``object_delay_samples`` 将位置更新与对象逆 QMF 的输出时刻对齐。
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_quantum_samples, object_delay_samples, obj,
trajectory_mode))
for obj in range(1, 16)
]
+259
View File
@@ -0,0 +1,259 @@
"""把 E-AC-3 核心 PCM 和 JOC 元数据重建为 LFE 加 15 路对象 PCM。
管线(每帧):
核心 5.1 PCM → QMF 分析 x → 填充器 z = Σ m·x(每对象)
→ 对象时域组装(前步、64 点 FFT、旋转、合成窗)→ ×16 + clamp
→ 对象波形 × joc_clipgain
+ LFE 专用 1217-sample 环形延迟
→ 16ch(ch0 = LFE, ch1-15 = 15 对象)→ ×output_scale
output_scale:
默认 1.0(0 dB)。用户选择的 dB 在命令行转换为 float32 系数;该系数
不进入 JOC 数学本体,也不是 joc_clipgain。
"""
import numpy as np
from joc_decode import parse_joc, diff_decode, dequantize
from joc_qmf import (N, QMF5_WINDOW, qmf_analysis_frame,
surround_post_frame, interp_matrix)
from evo_unpack import unpack_evolution
CORE_CHANNELS = [0, 1, 2, 4, 5] # L R C Ls Rs(EAC3 5.1 核心顺序)
class JocRenderer:
"""E-AC-3 JOC 帧渲染器。状态跨帧保持(FIFO/插值 prev/合成状态)。"""
def __init__(self, output_scale=1.0):
# 成品增益明确按 float32 运算;默认值为 1.0。
self.output_scale = np.float32(output_scale)
self._prev = {} # 每对象插值 prev(m 的帧末 sb 值)
self._synthesis_state = np.zeros((15, 640), dtype=np.float64)
# 分析 QMF:五路各自保留 9×64 FIFO;L/R/C 另有 10-timeslot 延迟。
self._analysis_fifo = np.zeros((5, 9, 64), dtype=np.float64)
self._analysis_delay = np.zeros((3, 10, 64), dtype=np.float32)
self._analysis_phase = np.float32(0.0625)
self._surround_qmf_delay = np.zeros((2, 10, 64), dtype=np.complex128)
self._surround_dc_hist = np.zeros((2, 20), dtype=np.complex128)
# ch0/LFE 使用循环历史,读指针恒落后写指针 1217 个采样。
# phase=1/16 与输出端 *16 抵消,净效果是 LFE 延迟。
self._lfe_delay = np.zeros(1217, dtype=np.float64)
self._last_x = None # 仅供逐槽诊断读取,不参与状态推进
self._rot = None # 每带旋转表
@staticmethod
def decode_payload(payload_bytes):
"""id14 载荷 → (parse 结果, mix_q, mix_dq)。"""
subs, _ = unpack_evolution(payload_bytes, loose=True)
out = parse_joc(subs[14])
mix_q = diff_decode(out)
mix_dq = dequantize(out, mix_q)
return out, mix_q, mix_dq
@staticmethod
def decode_subpayloads(subs):
"""已解析 EMDF 子载荷 → (parse 结果, mix_q, mix_dq)。"""
if 14 not in subs:
raise ValueError("EMDF 缺少 ID14/JOC")
out = parse_joc(subs[14])
mix_q = diff_decode(out)
mix_dq = dequantize(out, mix_q)
return out, mix_q, mix_dq
def qmf_x(self, bed5, phase_new=0.0625):
"""核心 5ch PCM → 对象矩阵使用的复数 QMF ``x``。
先以 float32 对当前帧应用 phase;phase 变化时仅前 256 个样本从旧值
线性过渡。缩放后的 L/R/C 延迟 10 槽,Ls/Rs 不延迟,再进入分析 QMF。
"""
pcm = np.asarray(bed5, dtype=np.float32)
if pcm.shape != (5, 1536):
raise ValueError(f"核心 5ch 帧应为 (5,1536),实际 {pcm.shape}")
new_phase = np.float32(phase_new)
old_phase = self._analysis_phase
gains = np.full(1536, new_phase, dtype=np.float32)
if old_phase != new_phase:
step = np.float32((new_phase - old_phase) / np.float32(256.0))
gains[:256] = old_phase + np.arange(256, dtype=np.float32) * step
scaled = np.multiply(pcm, gains[None, :], dtype=np.float32).reshape(5, 24, 64)
blocks = np.empty_like(scaled)
for ch in range(3):
delayed = np.concatenate((self._analysis_delay[ch], scaled[ch]), axis=0)
blocks[ch] = delayed[:24]
self._analysis_delay[ch] = delayed[24:]
blocks[3:] = scaled[3:]
x, self._analysis_fifo = qmf_analysis_frame(self._analysis_fifo, blocks)
self._analysis_phase = new_phase
x[3:], self._surround_qmf_delay, self._surround_dc_hist = surround_post_frame(
x[3:], self._surround_qmf_delay, self._surround_dc_hist)
return x
def object_z(self, out, mix_dq, x):
"""计算 ``z[obj] = Σ_ch m[ch,sb,ts]·x[ch,sb,ts]``。"""
z_all = {}
new_prev = {}
for obj, o in enumerate(out["objs"]):
if not o["present"]:
continue
dq = mix_dq[obj]
prev = self._prev.get(obj)
if prev is None:
prev = np.zeros((out["n_channels"], N), dtype=np.float64)
m = interp_matrix(o, dq, prev) # [ch][sb][ts](内部按 o["n_bands"] 映射)
z = np.sum(x * m, axis=0) # [sb][ts](复)
z_all[obj] = z
new_prev[obj] = m[:, :, -1]
self._prev.update(new_prev)
return z_all
def _rot_table_86840(self):
"""生成 ``θ=πk/128`` 的 ``0.5·(sin θ, cos θ)`` 旋转表。"""
if self._rot is None:
k = np.arange(64)
theta = np.pi * k / 128.0
self._rot = np.empty(128, dtype=np.float64)
self._rot[0::2] = 0.5 * np.sin(theta) # sin 分量
self._rot[1::2] = 0.5 * np.cos(theta) # cos 分量
return self._rot
@staticmethod
def _front_step(src):
"""重排 128 个交织复数分量:
outA[2k] = src[4k]、outA[2k+1] = −src[4k+1](区 [0:64]);
outB[126−2k] = src[4k+2]、outB[127−2k] = src[4k+3](区 [64:128] 倒序)。"""
src = np.asarray(src, dtype=np.float64)
zone = np.empty_like(src)
k = np.arange(32)
zone[..., 2 * k] = src[..., 4 * k]
zone[..., 2 * k + 1] = -src[..., 4 * k + 1]
zone[..., 126 - 2 * k] = src[..., 4 * k + 2]
zone[..., 127 - 2 * k] = src[..., 4 * k + 3]
return zone
@staticmethod
def _h880_vec(a2):
"""对 128 个 re/im 交织值执行标准 64 点复数 FFT。"""
a2 = np.asarray(a2, dtype=np.float64)
zc = a2[..., 0::2] + 1j * a2[..., 1::2]
f = np.fft.fft(zc, axis=-1)
a1 = np.empty_like(a2)
a1[..., 0::2] = f.real
a1[..., 1::2] = f.imag
return a1
@staticmethod
def _h0b0_vec(a2, a3):
"""NumPy 向量化的 ``out = 2·复乘(a2, a3)``,re/im 交织。"""
a2 = np.asarray(a2, dtype=np.float64)
a3 = np.asarray(a3, dtype=np.float64)
shape = a2.shape[:-1] + (16, 4)
v11 = a2[..., 1::2].reshape(shape)
v12 = a2[..., 0::2].reshape(shape)
v13 = a3[..., 0::2].reshape(a3.shape[:-1] + (16, 4))
v14 = a3[..., 1::2].reshape(a3.shape[:-1] + (16, 4))
v15 = (v11 * v14 - v12 * v13) * 2.0
v16 = (v12 * v14 + v11 * v13) * 2.0
out = np.empty_like(a2)
out[..., 0::2] = v16.reshape(a2.shape[:-1] + (64,))
out[..., 1::2] = v15.reshape(a2.shape[:-1] + (64,))
return out
@staticmethod
def _qmf5_vec(state, win_flat, rot):
"""推进合成窗状态并返回 64 个时域样本。"""
state = np.asarray(state, dtype=np.float64)
rot = np.asarray(rot, dtype=np.float64)
single = state.ndim == 1
if single:
state = state[None, :]
if rot.ndim == 1:
rot = np.broadcast_to(rot, (len(state), len(rot)))
rot2 = rot.reshape(len(state), 16, 8)
v22 = rot2[..., 0:8:2]
v19 = rot2[..., 1:8:2]
S = state[:, :576].reshape(len(state), 16, 9, 4)
w10 = win_flat[:640].reshape(10, 64)
idx = np.arange(16)[:, None] * 4 + np.arange(4)
w0 = w10[0, idx][None, ...]
w_all = w10[1:10, idx].transpose(1, 0, 2)[None, ...]
out64 = (2.0 * (w0 * v22 + S[:, :, 0])).reshape(len(state), 64)
# 输出使用窗行 0,状态第 0 行从窗行 1 开始推进。
S[:, :, 0] = w_all[:, :, 0] * v19 + S[:, :, 1]
for k in range(7):
alt = v22 if k % 2 == 0 else v19
S[:, :, k + 1] = w_all[:, :, k + 1] * alt + S[:, :, k + 2]
S[:, :, 8] = w_all[:, :, 8] * v19
return out64[0] if single else out64
def dll_synth_objects(self, z_all):
"""批量执行对象逆 QMF,返回对象 PCM 字典。
每个对象保持独立合成状态,64 点 FFT 和窗核在对象维批量计算。
"""
object_ids = sorted(z_all)
if not object_ids:
return {}
z = np.stack([z_all[obj] for obj in object_ids], axis=0)
state = self._synthesis_state[object_ids].copy()
rot868 = self._rot_table_86840()
pcm = np.zeros((len(object_ids), 1536), dtype=np.float64)
for tsg in range(6):
ring = np.zeros((len(object_ids), 512), dtype=np.float64)
chunk = z[:, :, tsg * 4:tsg * 4 + 4].transpose(0, 2, 1)
slots = ring.reshape(len(object_ids), 4, 128)
slots[..., 0::2] = chunk.real
slots[..., 1::2] = chunk.imag
for it in range(4):
zone = self._front_step(ring[:, 128 * it:128 * it + 128])
zone = self._h0b0_vec(self._h880_vec(zone), rot868)
ring[:, 64 * it:64 * it + 64] = self._qmf5_vec(state, QMF5_WINDOW, zone)
pcm[:, tsg * 256:(tsg + 1) * 256] = np.clip(16.0 * ring[:, :256], -1.0, 1.0)
self._synthesis_state[object_ids] = state
return {obj: pcm[i] for i, obj in enumerate(object_ids)}
def dll_synth_object(self, obj, z):
"""单对象兼容入口;整帧渲染使用对象维批量实现。"""
return self.dll_synth_objects({obj: z})[obj]
@staticmethod
def _win_flat():
"""返回逆 QMF 使用的 640 项有效合成窗表。"""
return QMF5_WINDOW
def decode_lfe(self, lfe_pcm):
"""将核心 ch3/LFE 经过跨帧 1217-sample 延迟后输出到 ch0。"""
current = np.asarray(lfe_pcm, dtype=np.float64)
if current.shape != (1536,):
raise ValueError(f"LFE 帧应为 (1536,),实际 {current.shape}")
delayed = np.concatenate([self._lfe_delay, current])
out = np.clip(delayed[:1536], -1.0, 1.0)
self._lfe_delay = delayed[-1217:].copy()
return out
def render_frame(self, payload_bytes, bed5_pcm, lfe_pcm=None):
"""payload_bytes = evolution 载荷(含 id14 JOC);bed5_pcm = 5×1536 核心 PCM。
lfe_pcm = 1536 核心 LFE;提供时生成 ch0,省略时 ch0 静音
(保留给只验证对象/z 链的诊断脚本)。
返回 (pcm16, z_all):pcm16 = 16×1536(ch0 LFE + 15 对象)f32,
已用 FP32 系数乘 output_scale。"""
subs, _ = unpack_evolution(payload_bytes, loose=True)
return self.render_subpayloads(subs, bed5_pcm, lfe_pcm)
def render_subpayloads(self, subs, bed5_pcm, lfe_pcm=None):
"""以已拆出的 EMDF payload 字典渲染一帧,避免绑定 transport 容器。"""
out, mix_q, mix_dq = self.decode_subpayloads(subs)
x = self.qmf_x(bed5_pcm)
self._last_x = x.copy()
z_all = self.object_z(out, mix_dq, x)
# joc_clipgain 在对象逆 QMF 后应用,并与用户 output_scale 分离。
objs_pcm = self.dll_synth_objects(z_all)
pcm16 = np.zeros((16, 1536), dtype=np.float64)
pcm16[0] = self.decode_lfe(lfe_pcm) if lfe_pcm is not None else 0.0
for obj, p in objs_pcm.items():
pcm16[obj + 1] = p * out["clipgain"]
pcm16_f32 = np.asarray(pcm16, dtype=np.float32)
return np.multiply(pcm16_f32, self.output_scale, dtype=np.float32), z_all
+71
View File
@@ -0,0 +1,71 @@
"""Unified auto/native/python entry point for float64 speaker rendering."""
from __future__ import annotations
import time
import numpy as np
from speaker_layouts import SpeakerLayout, get_speaker_layout
from speaker_native_renderer import NativeSpeakerRenderer
from speaker_renderer import PythonSpeakerRenderer, payload_events_from_index, render_objects16
FRAME_SAMPLES = 1536
def create_speaker_renderer(layout: str | SpeakerLayout, *, backend="auto", native_library=None):
"""Create a stateful speaker renderer and return ``(renderer, info)``."""
if backend not in ("auto", "native", "python"):
raise ValueError(f"unknown speaker backend: {backend}")
target = get_speaker_layout(layout) if isinstance(layout, str) else layout
fallback_reason = None
if backend in ("auto", "native"):
try:
renderer = NativeSpeakerRenderer(target, native_library)
return renderer, {
"name": "native",
"layout": target.name,
"library": str(renderer.library_path),
"fallback_reason": None,
}
except (OSError, RuntimeError) as exc:
fallback_reason = str(exc)
return PythonSpeakerRenderer(target), {
"name": "python",
"layout": target.name,
"library": None,
"fallback_reason": fallback_reason,
}
def render_speaker_layout(objects16, index, layout: str | SpeakerLayout, *,
backend="auto", native_library=None,
metadata_offset=1473):
"""Render a complete indexed object stream and return ``(pcm64, info)``."""
target = get_speaker_layout(layout) if isinstance(layout, str) else layout
source = np.asarray(objects16)
if source.ndim != 2 or source.shape[1] != 16:
raise ValueError(f"objects16 must have shape [samples,16], got {source.shape}")
if len(source) % FRAME_SAMPLES:
raise ValueError("objects16 sample count must be divisible by 1536")
frames = len(source) // FRAME_SAMPLES
if frames > len(index.rows):
raise ValueError(f"metadata has {len(index.rows)} frames, PCM needs {frames}")
renderer, info = create_speaker_renderer(
target, backend=backend, native_library=native_library)
output = np.empty((len(source), target.channel_count), dtype=np.float64)
started = time.perf_counter()
try:
for frame in range(frames):
start = frame * FRAME_SAMPLES
subs = index.subpayloads(index.rows[frame])
output[start:start + FRAME_SAMPLES] = renderer.render_frame(
source[start:start + FRAME_SAMPLES], subs.get(11), metadata_offset)
finally:
close = getattr(renderer, "close", None)
if close is not None:
close()
info = dict(info)
info["seconds"] = time.perf_counter() - started
return output, info
+764
View File
@@ -0,0 +1,764 @@
"""Standard speaker-layout geometry used by the spatial renderer.
Coordinates are unsigned Q15 room coordinates. This is geometry/topology data,
not a sampled gain table.
"""
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class RegionGeometry:
coordinates_q15: tuple[tuple[int, int, int], ...]
speaker_ids: tuple[int, ...]
axis0_groups: tuple[tuple[int, ...], ...]
axis1_groups: tuple[tuple[int, ...], ...]
mode: int
@dataclass(frozen=True)
class SpeakerLayout:
name: str
out_ch_config: int
speaker_bitfield: int
channels: tuple[str, ...]
internal_to_standard: tuple[int, ...]
regions: tuple[RegionGeometry, ...]
@property
def channel_count(self) -> int:
return len(self.channels)
_RAW_LAYOUTS = {'20': {'out_ch_config': 0,
'speaker_bitfield': 1,
'channels': ('L', 'R'),
'internal_to_standard': (0, 1),
'regions': ({'coordinates_q15': ((0, 0, 0), (32767, 0, 0)),
'speaker_ids': (0, 1),
'axis0_groups': ((0, 1),),
'axis1_groups': (),
'mode': 1},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0)),
'speaker_ids': (0, 1),
'axis0_groups': ((0, 1),),
'axis1_groups': (),
'mode': 1},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0)),
'speaker_ids': (0, 1),
'axis0_groups': ((0, 1),),
'axis1_groups': (),
'mode': 1},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0)),
'speaker_ids': (0, 1),
'axis0_groups': ((0, 1),),
'axis1_groups': (),
'mode': 1},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0)),
'speaker_ids': (0, 1),
'axis0_groups': ((0, 1),),
'axis1_groups': (),
'mode': 1},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0)),
'speaker_ids': (0, 1),
'axis0_groups': ((0, 1),),
'axis1_groups': (),
'mode': 1},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0)),
'speaker_ids': (0, 1),
'axis0_groups': ((0, 1),),
'axis1_groups': (),
'mode': 1})},
'31': {'out_ch_config': 3,
'speaker_bitfield': 7,
'channels': ('L', 'R', 'C', 'LFE'),
'internal_to_standard': (0, 1, 2, 3),
'regions': ({'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0)),
'speaker_ids': (0, 1, 2),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': (),
'mode': 1},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0)),
'speaker_ids': (0, 1, 2),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': (),
'mode': 1},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0)),
'speaker_ids': (0, 1, 2),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': (),
'mode': 1},
{'coordinates_q15': ((16384, 0, 0),),
'speaker_ids': (2,),
'axis0_groups': ((0,),),
'axis1_groups': (),
'mode': 1},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0)),
'speaker_ids': (0, 1, 2),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': (),
'mode': 1},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0)),
'speaker_ids': (0, 1, 2),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': (),
'mode': 1},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0)),
'speaker_ids': (0, 1, 2),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': (),
'mode': 1})},
'51': {'out_ch_config': 7,
'speaker_bitfield': 15,
'channels': ('L', 'R', 'C', 'LFE', 'Ls', 'Rs'),
'internal_to_standard': (0, 1, 2, 3, 4, 5),
'regions': ({'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0), (0, 32767, 0), (32767, 32767, 0)),
'speaker_ids': (0, 1, 2, 4, 5),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': (),
'mode': 2},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0), (0, 32767, 0), (32767, 32767, 0)),
'speaker_ids': (0, 1, 2, 4, 5),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': (),
'mode': 2},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0), (0, 32767, 0), (32767, 32767, 0)),
'speaker_ids': (0, 1, 2, 4, 5),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': (),
'mode': 2},
{'coordinates_q15': ((16384, 0, 0), (0, 32767, 0), (32767, 32767, 0)),
'speaker_ids': (2, 4, 5),
'axis0_groups': ((0,), (1, 2)),
'axis1_groups': (),
'mode': 2},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0)),
'speaker_ids': (0, 1, 2),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': (),
'mode': 1},
{'coordinates_q15': ((0, 32767, 0), (32767, 32767, 0)),
'speaker_ids': (4, 5),
'axis0_groups': ((0, 1),),
'axis1_groups': (),
'mode': 1},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0)),
'speaker_ids': (0, 1, 2),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': (),
'mode': 1})},
'71': {'out_ch_config': 11,
'speaker_bitfield': 31,
'channels': ('L', 'R', 'C', 'LFE', 'Ls', 'Rs', 'Lrs', 'Rrs'),
'internal_to_standard': (0, 1, 2, 3, 6, 7, 4, 5),
'regions': ({'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 16384, 0),
(32767, 16384, 0),
(0, 32767, 0),
(32767, 32767, 0)),
'speaker_ids': (0, 1, 2, 4, 5, 6, 7),
'axis0_groups': ((0, 2, 1), (3, 4), (5, 6)),
'axis1_groups': (),
'mode': 2},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0), (0, 16384, 0), (32767, 16384, 0)),
'speaker_ids': (0, 1, 2, 4, 5),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': (),
'mode': 2},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0), (0, 32767, 0), (32767, 32767, 0)),
'speaker_ids': (0, 1, 2, 6, 7),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': (),
'mode': 2},
{'coordinates_q15': ((16384, 0, 0), (0, 32767, 0), (32767, 32767, 0)),
'speaker_ids': (2, 6, 7),
'axis0_groups': ((0,), (1, 2)),
'axis1_groups': (),
'mode': 2},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0)),
'speaker_ids': (0, 1, 2),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': (),
'mode': 1},
{'coordinates_q15': ((0, 16384, 0), (32767, 16384, 0), (0, 32767, 0), (32767, 32767, 0)),
'speaker_ids': (4, 5, 6, 7),
'axis0_groups': ((0, 1), (2, 3)),
'axis1_groups': (),
'mode': 2},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0)),
'speaker_ids': (0, 1, 2),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': (),
'mode': 1})},
'512': {'out_ch_config': 13,
'speaker_bitfield': 1039,
'channels': ('L', 'R', 'C', 'LFE', 'Ls', 'Rs', 'Ltm', 'Rtm'),
'internal_to_standard': (0, 1, 2, 3, 4, 5, 6, 7),
'regions': ({'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 16384, 32767),
(24840, 16384, 32767)),
'speaker_ids': (0, 1, 2, 4, 5, 6, 7),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': ((5, 6),),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 16384, 32767),
(24840, 16384, 32767)),
'speaker_ids': (0, 1, 2, 4, 5, 6, 7),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': ((5, 6),),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 16384, 32767),
(24840, 16384, 32767)),
'speaker_ids': (0, 1, 2, 4, 5, 6, 7),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': ((5, 6),),
'mode': 3},
{'coordinates_q15': ((16384, 0, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 16384, 32767),
(24840, 16384, 32767)),
'speaker_ids': (2, 4, 5, 6, 7),
'axis0_groups': ((0,), (1, 2)),
'axis1_groups': ((3, 4),),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(7928, 16384, 32767),
(24840, 16384, 32767)),
'speaker_ids': (0, 1, 2, 6, 7),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': ((3, 4),),
'mode': 3},
{'coordinates_q15': ((0, 32767, 0),
(32767, 32767, 0),
(7928, 16384, 32767),
(24840, 16384, 32767)),
'speaker_ids': (4, 5, 6, 7),
'axis0_groups': ((0, 1),),
'axis1_groups': ((2, 3),),
'mode': 3},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0)),
'speaker_ids': (0, 1, 2),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': (),
'mode': 1})},
'514': {'out_ch_config': 14,
'speaker_bitfield': 2575,
'channels': ('L', 'R', 'C', 'LFE', 'Ls', 'Rs', 'Ltf', 'Rtf', 'Ltr', 'Rtr'),
'internal_to_standard': (0, 1, 2, 3, 4, 5, 6, 7, 8, 9),
'regions': ({'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (0, 1, 2, 4, 5, 6, 7, 8, 9),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': ((5, 6), (7, 8)),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (0, 1, 2, 4, 5, 6, 7, 8, 9),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': ((5, 6), (7, 8)),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (0, 1, 2, 4, 5, 6, 7, 8, 9),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': ((5, 6), (7, 8)),
'mode': 3},
{'coordinates_q15': ((16384, 0, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (2, 4, 5, 6, 7, 8, 9),
'axis0_groups': ((0,), (1, 2)),
'axis1_groups': ((3, 4), (5, 6)),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (0, 1, 2, 6, 7, 8, 9),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': ((3, 4), (5, 6)),
'mode': 3},
{'coordinates_q15': ((0, 32767, 0),
(32767, 32767, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (4, 5, 6, 7, 8, 9),
'axis0_groups': ((0, 1),),
'axis1_groups': ((2, 3), (4, 5)),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(7928, 7928, 32767),
(24840, 7928, 32767)),
'speaker_ids': (0, 1, 2, 6, 7),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': ((3, 4),),
'mode': 3})},
'712': {'out_ch_config': 15,
'speaker_bitfield': 1055,
'channels': ('L', 'R', 'C', 'LFE', 'Ls', 'Rs', 'Lrs', 'Rrs', 'Ltm', 'Rtm'),
'internal_to_standard': (0, 1, 2, 3, 6, 7, 4, 5, 8, 9),
'regions': ({'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 16384, 0),
(32767, 16384, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 16384, 32767),
(24840, 16384, 32767)),
'speaker_ids': (0, 1, 2, 4, 5, 6, 7, 8, 9),
'axis0_groups': ((0, 2, 1), (3, 4), (5, 6)),
'axis1_groups': ((7, 8),),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 16384, 0),
(32767, 16384, 0),
(7928, 16384, 32767),
(24840, 16384, 32767)),
'speaker_ids': (0, 1, 2, 4, 5, 8, 9),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': ((5, 6),),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 16384, 32767),
(24840, 16384, 32767)),
'speaker_ids': (0, 1, 2, 6, 7, 8, 9),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': ((5, 6),),
'mode': 3},
{'coordinates_q15': ((16384, 0, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 16384, 32767),
(24840, 16384, 32767)),
'speaker_ids': (2, 6, 7, 8, 9),
'axis0_groups': ((0,), (1, 2)),
'axis1_groups': ((3, 4),),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(7928, 16384, 32767),
(24840, 16384, 32767)),
'speaker_ids': (0, 1, 2, 8, 9),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': ((3, 4),),
'mode': 3},
{'coordinates_q15': ((0, 16384, 0),
(32767, 16384, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 16384, 32767),
(24840, 16384, 32767)),
'speaker_ids': (4, 5, 6, 7, 8, 9),
'axis0_groups': ((0, 1), (2, 3)),
'axis1_groups': ((4, 5),),
'mode': 3},
{'coordinates_q15': ((0, 0, 0), (32767, 0, 0), (16384, 0, 0)),
'speaker_ids': (0, 1, 2),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': (),
'mode': 1})},
'714': {'out_ch_config': 16,
'speaker_bitfield': 2591,
'channels': ('L', 'R', 'C', 'LFE', 'Ls', 'Rs', 'Lrs', 'Rrs', 'Ltf', 'Rtf', 'Ltr', 'Rtr'),
'internal_to_standard': (0, 1, 2, 3, 6, 7, 4, 5, 8, 9, 10, 11),
'regions': ({'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 16384, 0),
(32767, 16384, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11),
'axis0_groups': ((0, 2, 1), (3, 4), (5, 6)),
'axis1_groups': ((7, 8), (9, 10)),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 16384, 0),
(32767, 16384, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (0, 1, 2, 4, 5, 8, 9, 10, 11),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': ((5, 6), (7, 8)),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (0, 1, 2, 6, 7, 8, 9, 10, 11),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': ((5, 6), (7, 8)),
'mode': 3},
{'coordinates_q15': ((16384, 0, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (2, 6, 7, 8, 9, 10, 11),
'axis0_groups': ((0,), (1, 2)),
'axis1_groups': ((3, 4), (5, 6)),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (0, 1, 2, 8, 9, 10, 11),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': ((3, 4), (5, 6)),
'mode': 3},
{'coordinates_q15': ((0, 16384, 0),
(32767, 16384, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (4, 5, 6, 7, 8, 9, 10, 11),
'axis0_groups': ((0, 1), (2, 3)),
'axis1_groups': ((4, 5), (6, 7)),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(7928, 7928, 32767),
(24840, 7928, 32767)),
'speaker_ids': (0, 1, 2, 8, 9),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': ((3, 4),),
'mode': 3})},
'914': {'out_ch_config': 19,
'speaker_bitfield': 2719,
'channels': ('L', 'R', 'C', 'LFE', 'Ls', 'Rs', 'Lrs', 'Rrs', 'Lw', 'Rw', 'Ltf', 'Rtf', 'Ltr', 'Rtr'),
'internal_to_standard': (0, 1, 2, 3, 6, 7, 4, 5, 10, 11, 12, 13, 8, 9),
'regions': ({'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 16384, 0),
(32767, 16384, 0),
(0, 32767, 0),
(32767, 32767, 0),
(0, 5285, 0),
(32767, 5285, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13),
'axis0_groups': ((0, 2, 1), (7, 8), (3, 4), (5, 6)),
'axis1_groups': ((9, 10), (11, 12)),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 16384, 0),
(32767, 16384, 0),
(0, 5285, 0),
(32767, 5285, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (0, 1, 2, 4, 5, 8, 9, 10, 11, 12, 13),
'axis0_groups': ((0, 2, 1), (5, 6), (3, 4)),
'axis1_groups': ((7, 8), (9, 10)),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (0, 1, 2, 6, 7, 10, 11, 12, 13),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': ((5, 6), (7, 8)),
'mode': 3},
{'coordinates_q15': ((16384, 0, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (2, 6, 7, 10, 11, 12, 13),
'axis0_groups': ((0,), (1, 2)),
'axis1_groups': ((3, 4), (5, 6)),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (0, 1, 2, 10, 11, 12, 13),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': ((3, 4), (5, 6)),
'mode': 3},
{'coordinates_q15': ((0, 16384, 0),
(32767, 16384, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (4, 5, 6, 7, 10, 11, 12, 13),
'axis0_groups': ((0, 1), (2, 3)),
'axis1_groups': ((4, 5), (6, 7)),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 5285, 0),
(32767, 5285, 0),
(7928, 7928, 32767),
(24840, 7928, 32767)),
'speaker_ids': (0, 1, 2, 8, 9, 10, 11),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': ((5, 6),),
'mode': 3})},
'916': {'out_ch_config': 20,
'speaker_bitfield': 3743,
'channels': ('L',
'R',
'C',
'LFE',
'Ls',
'Rs',
'Lrs',
'Rrs',
'Lw',
'Rw',
'Ltf',
'Rtf',
'Ltm',
'Rtm',
'Ltr',
'Rtr'),
'internal_to_standard': (0, 1, 2, 3, 6, 7, 4, 5, 10, 11, 14, 15, 12, 13, 8, 9),
'regions': ({'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 16384, 0),
(32767, 16384, 0),
(0, 32767, 0),
(32767, 32767, 0),
(0, 5285, 0),
(32767, 5285, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 16384, 32767),
(24840, 16384, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15),
'axis0_groups': ((0, 2, 1), (7, 8), (3, 4), (5, 6)),
'axis1_groups': ((9, 10), (11, 12), (13, 14)),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 16384, 0),
(32767, 16384, 0),
(0, 5285, 0),
(32767, 5285, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 16384, 32767),
(24840, 16384, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (0, 1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15),
'axis0_groups': ((0, 2, 1), (5, 6), (3, 4)),
'axis1_groups': ((7, 8), (9, 10), (11, 12)),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 16384, 32767),
(24840, 16384, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (0, 1, 2, 6, 7, 10, 11, 12, 13, 14, 15),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': ((5, 6), (7, 8), (9, 10)),
'mode': 3},
{'coordinates_q15': ((16384, 0, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 16384, 32767),
(24840, 16384, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (2, 6, 7, 10, 11, 12, 13, 14, 15),
'axis0_groups': ((0,), (1, 2)),
'axis1_groups': ((3, 4), (5, 6), (7, 8)),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 16384, 32767),
(24840, 16384, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (0, 1, 2, 10, 11, 12, 13, 14, 15),
'axis0_groups': ((0, 2, 1),),
'axis1_groups': ((3, 4), (5, 6), (7, 8)),
'mode': 3},
{'coordinates_q15': ((0, 16384, 0),
(32767, 16384, 0),
(0, 32767, 0),
(32767, 32767, 0),
(7928, 7928, 32767),
(24840, 7928, 32767),
(7928, 16384, 32767),
(24840, 16384, 32767),
(7928, 24840, 32767),
(24840, 24840, 32767)),
'speaker_ids': (4, 5, 6, 7, 10, 11, 12, 13, 14, 15),
'axis0_groups': ((0, 1), (2, 3)),
'axis1_groups': ((4, 5), (6, 7), (8, 9)),
'mode': 3},
{'coordinates_q15': ((0, 0, 0),
(32767, 0, 0),
(16384, 0, 0),
(0, 5285, 0),
(32767, 5285, 0),
(7928, 7928, 32767),
(24840, 7928, 32767)),
'speaker_ids': (0, 1, 2, 8, 9, 10, 11),
'axis0_groups': ((0, 2, 1), (3, 4)),
'axis1_groups': ((5, 6),),
'mode': 3})}}
def _make_layout(name: str, item: dict) -> SpeakerLayout:
return SpeakerLayout(
name=name,
out_ch_config=item["out_ch_config"],
speaker_bitfield=item["speaker_bitfield"],
channels=item["channels"],
internal_to_standard=item["internal_to_standard"],
regions=tuple(RegionGeometry(**region) for region in item["regions"]),
)
SPEAKER_LAYOUTS = {name: _make_layout(name, item) for name, item in _RAW_LAYOUTS.items()}
SPEAKER_LAYOUT_ALIASES = {
"2.0": "20",
"3.1": "31",
"5.1": "51",
"7.1": "71",
"5.1.2": "512",
"5.1.4": "514",
"7.1.2": "712",
"7.1.4": "714",
"9.1.4": "914",
"9.1.6": "916",
}
SPEAKER_LAYOUT_CHOICES = tuple(SPEAKER_LAYOUT_ALIASES)
SPEAKER_LAYOUT_DISPLAY_NAMES = {value: key for key, value in SPEAKER_LAYOUT_ALIASES.items()}
def get_speaker_layout(name: str) -> SpeakerLayout:
key = SPEAKER_LAYOUT_ALIASES.get(name, name)
try:
return SPEAKER_LAYOUTS[key]
except KeyError as exc:
raise ValueError(f"unsupported speaker layout: {name}") from exc
def speaker_layout_display_name(layout: str | SpeakerLayout) -> str:
target = get_speaker_layout(layout) if isinstance(layout, str) else layout
return SPEAKER_LAYOUT_DISPLAY_NAMES[target.name]
+154
View File
@@ -0,0 +1,154 @@
"""ctypes bridge for the float64 native object-to-speaker renderer."""
from __future__ import annotations
import ctypes
from pathlib import Path
import numpy as np
from native_renderer import ABI_VERSION, find_native_library
from oamd_bits import JocFieldState, frame_update
from speaker_layouts import SpeakerLayout, get_speaker_layout
FRAME_SAMPLES = 1536
INPUT_CHANNELS = 16
OBJECTS = 15
COORDINATES = 3
class NativeSpeakerRenderer:
def __init__(self, layout: str | SpeakerLayout, library_path=None):
self.layout = get_speaker_layout(layout) if isinstance(layout, str) else layout
self.library_path = find_native_library(library_path)
self._lib = ctypes.CDLL(str(self.library_path))
self._bind()
version = int(self._lib.ejoc_abi_version())
if version != ABI_VERSION:
raise RuntimeError(f"native ABI mismatch: expected {ABI_VERSION}, got {version}")
channels = int(self._lib.ejoc_speaker_layout_channel_count(self.layout.speaker_bitfield))
if channels != self.layout.channel_count:
raise RuntimeError(
f"native layout channel count mismatch: expected {self.layout.channel_count}, got {channels}"
)
self._handle = self._lib.ejoc_speaker_renderer_create(self.layout.speaker_bitfield)
if not self._handle:
raise RuntimeError(f"native speaker renderer rejected mask 0x{self.layout.speaker_bitfield:X}")
self._state = JocFieldState()
def _bind(self):
void_p = ctypes.c_void_p
self._lib.ejoc_abi_version.argtypes = []
self._lib.ejoc_abi_version.restype = ctypes.c_uint32
self._lib.ejoc_speaker_layout_channel_count.argtypes = [ctypes.c_uint32]
self._lib.ejoc_speaker_layout_channel_count.restype = ctypes.c_uint32
self._lib.ejoc_speaker_renderer_create.argtypes = [ctypes.c_uint32]
self._lib.ejoc_speaker_renderer_create.restype = void_p
self._lib.ejoc_speaker_renderer_destroy.argtypes = [void_p]
self._lib.ejoc_speaker_renderer_destroy.restype = None
self._lib.ejoc_speaker_renderer_reset.argtypes = [void_p]
self._lib.ejoc_speaker_renderer_reset.restype = ctypes.c_int
self._lib.ejoc_speaker_renderer_last_error.argtypes = [void_p]
self._lib.ejoc_speaker_renderer_last_error.restype = ctypes.c_char_p
self._lib.ejoc_speaker_renderer_process.argtypes = [
void_p,
ctypes.POINTER(ctypes.c_float),
ctypes.c_uint32,
ctypes.c_uint32,
ctypes.POINTER(ctypes.c_uint32),
ctypes.POINTER(ctypes.c_uint32),
ctypes.POINTER(ctypes.c_uint16),
ctypes.POINTER(ctypes.c_uint8),
ctypes.POINTER(ctypes.c_uint8),
ctypes.POINTER(ctypes.c_double),
ctypes.POINTER(ctypes.c_double),
]
self._lib.ejoc_speaker_renderer_process.restype = ctypes.c_int
def _raise(self, operation, status):
message = self._lib.ejoc_speaker_renderer_last_error(self._handle)
detail = (message or b"").decode("utf-8", "replace")
raise RuntimeError(f"native speaker renderer {operation} failed ({status}): {detail}")
def reset(self):
if not self._handle:
raise RuntimeError("native speaker renderer is closed")
status = self._lib.ejoc_speaker_renderer_reset(self._handle)
if status:
self._raise("reset", status)
self._state = JocFieldState()
def close(self):
if self._handle:
self._lib.ejoc_speaker_renderer_destroy(self._handle)
self._handle = None
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
self.close()
def __del__(self):
try:
self.close()
except Exception:
pass
def _metadata_arrays(self, events):
events = list(events)
count = len(events)
if not count:
return count, None, None, None
offsets = np.empty(count, dtype=np.uint32)
ramps = np.empty(count, dtype=np.uint32)
positions = np.empty((count, OBJECTS, COORDINATES), dtype=np.uint16)
for event_index, (outer_offset, payload) in enumerate(events):
update = frame_update(payload)
self._state.apply(update["values"])
offsets[event_index] = int(outer_offset) + int(update["block_offset_samples"])
ramps[event_index] = int(update["ramp_duration_samples"])
for object_index in range(1, OBJECTS + 1):
positions[event_index, object_index - 1] = (
self._state.q[(object_index, "q1")],
self._state.q[(object_index, "q2")],
self._state.q[(object_index, "q3")],
)
return count, offsets, ramps, positions
def process(self, objects16, events=()):
if not self._handle:
raise RuntimeError("native speaker renderer is closed")
source = np.ascontiguousarray(objects16, dtype=np.float32)
if source.ndim != 2 or source.shape[1] != INPUT_CHANNELS:
raise ValueError(f"objects16 must have shape [samples,16], got {source.shape}")
if len(source) % 32:
raise ValueError("sample count must be a multiple of 32")
count, offsets, ramps, positions = self._metadata_arrays(events)
output = np.empty((len(source), self.layout.channel_count), dtype=np.float64)
null_u32 = ctypes.POINTER(ctypes.c_uint32)()
null_u16 = ctypes.POINTER(ctypes.c_uint16)()
null_u8 = ctypes.POINTER(ctypes.c_uint8)()
null_f64 = ctypes.POINTER(ctypes.c_double)()
status = self._lib.ejoc_speaker_renderer_process(
self._handle,
source.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
len(source),
count,
offsets.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)) if count else null_u32,
ramps.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)) if count else null_u32,
positions.ctypes.data_as(ctypes.POINTER(ctypes.c_uint16)) if count else null_u16,
null_u8,
null_u8,
null_f64,
output.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
)
if status:
self._raise("process", status)
return output
def render_frame(self, objects16, payload=None, metadata_offset=1473):
source = np.asarray(objects16)
if source.shape != (FRAME_SAMPLES, INPUT_CHANNELS):
raise ValueError(f"frame must have shape (1536,16), got {source.shape}")
events = () if payload is None else ((int(metadata_offset), bytes(payload)),)
return self.process(source, events)
+311
View File
@@ -0,0 +1,311 @@
"""High-precision object-to-speaker renderer.
All spatial calculations, gain ramps, and object accumulation use float64. Input
PCM may be float32, but precision is reduced only when the caller explicitly
writes a lower-precision output format.
"""
from __future__ import annotations
from collections import defaultdict
import math
from typing import Iterable
import numpy as np
from oamd_bits import JocFieldState, frame_update
from speaker_layouts import RegionGeometry, SpeakerLayout, get_speaker_layout
Q15_SCALE = 32768.0
Q15_MAX = 32767.0 / Q15_SCALE
GAIN_SNAP_THRESHOLD = 1.0e-4
def expand_speaker_bitfield(compact_mask: int, center_height: bool = False) -> int:
"""Expand the compact target-layout bitfield into individual speakers."""
expansions = (
0x00000003, 0x00000004, 0x00000008, 0x00000030,
0x000000C0, 0x00000100, 0x00000600, 0x00001800,
0x00006000, 0x00018000, 0x00060000, 0x00180000,
0x00600000, 0x01800000, 0x06000000, 0x18000000,
0x60000000, 0x080000000, 0x600000000, 0x800000000,
0x1000000000, 0x2000000000,
)
expanded = 0
for bit, value in enumerate(expansions):
if int(compact_mask) & (1 << bit):
expanded |= value
if center_height:
expanded |= 0x4000000000
return expanded
def layout_attenuation_db(compact_mask: int) -> float:
"""Compute the layout-dependent maximum positional compensation in dB."""
expanded = expand_speaker_bitfield(compact_mask)
height_channels = 2 * sum((expanded >> bit) & 1 for bit in (13, 15, 17, 19, 21))
floor_channels = ((expanded >> 8) & 1) + 2 * sum(
(expanded >> bit) & 1 for bit in (31, 4, 6, 11, 25, 27, 29, 33)
)
height_factor = min(height_channels / 4.0, 1.0)
floor_factor = min(floor_channels / 4.0, 1.0)
return -max(4.5 - 1.5 * height_factor - 3.0 * floor_factor, 0.0)
def floor_y_exponent(compact_mask: int, metadata_scaling_enabled: bool = True) -> int:
if not metadata_scaling_enabled:
return 0
low_mask = expand_speaker_bitfield(compact_mask) & 0xFFFFFFFF
return int(bool(low_mask & 0x130) and not bool(low_mask & 0x18C0))
def position_gain(compact_mask: int, v: float, w: float) -> float:
"""Return the high-precision position-dependent layout compensation."""
y_term = min(max(float(v) / 0.6, 0.0), 1.0)
z_term = min(max((float(w) - 0.2) / 0.8, 0.0), 1.0)
amount = min(max(y_term + z_term, 0.0), 1.0)
return math.pow(10.0, layout_attenuation_db(compact_mask) * amount / 20.0)
def equal_power_pair(position: float) -> tuple[float, float]:
angle = math.pi * 0.5 * float(position)
return math.cos(angle), math.sin(angle)
def _coordinates(region: RegionGeometry) -> np.ndarray:
return np.asarray(region.coordinates_q15, dtype=np.float64) / Q15_SCALE
def _normalized(value: float, lower: float, upper: float) -> float:
return (float(value) - float(lower)) / (float(upper) - float(lower))
def _axis0_gains(region: RegionGeometry, coordinates: np.ndarray, value: float) -> np.ndarray:
result = np.zeros(len(region.speaker_ids), dtype=np.float64)
position = float(value)
for group in region.axis0_groups:
if not group:
continue
first, last = group[0], group[-1]
if position <= coordinates[first, 0]:
result[first] = 1.0
continue
if position >= coordinates[last, 0]:
result[last] = 1.0
continue
for lower_index, upper_index in zip(group, group[1:]):
lower = coordinates[lower_index, 0]
upper = coordinates[upper_index, 0]
if position > lower and position <= upper:
result[lower_index], result[upper_index] = equal_power_pair(
_normalized(position, lower, upper)
)
break
return result
def _axis1_gains(region: RegionGeometry, coordinates: np.ndarray, groups,
value: float) -> np.ndarray:
result = np.zeros(len(region.speaker_ids), dtype=np.float64)
if not groups:
return result
position = float(value)
first_value = coordinates[groups[0][0], 1]
last_value = coordinates[groups[-1][0], 1]
if position <= first_value:
result[list(groups[0])] = 1.0
return result
if position > last_value:
result[list(groups[-1])] = 1.0
return result
for lower_group, upper_group in zip(groups, groups[1:]):
lower = coordinates[lower_group[0], 1]
upper = coordinates[upper_group[0], 1]
if position >= lower and position <= upper:
lower_gain, upper_gain = equal_power_pair(_normalized(position, lower, upper))
result[list(lower_group)] = lower_gain
result[list(upper_group)] = upper_gain
break
return result
def _plane_gains(region: RegionGeometry, coordinates: np.ndarray, groups,
u: float, v: float, mode: int) -> np.ndarray:
gains = _axis0_gains(
RegionGeometry(region.coordinates_q15, region.speaker_ids, tuple(groups), (), region.mode),
coordinates,
u,
)
if mode >= 2:
gains *= _axis1_gains(region, coordinates, groups, v)
return gains
def render_point_gains(layout: str | SpeakerLayout, u: float, v: float, w: float,
*, region_index: int = 0, enable_height: bool = True,
object_gain: float = 1.0, standard_order: bool = True) -> np.ndarray:
"""Render one point object to a target speaker layout using float64."""
target = get_speaker_layout(layout) if isinstance(layout, str) else layout
region = target.regions[int(region_index)]
coordinates = _coordinates(region)
floor_v = min(max(math.ldexp(float(v), floor_y_exponent(target.speaker_bitfield)), 0.0), 1.0)
floor = _plane_gains(region, coordinates, region.axis0_groups, u, floor_v, region.mode)
point_gains = floor
if region.mode == 3:
top = _plane_gains(region, coordinates, region.axis1_groups, u, float(v), 3)
height = min(max(float(w) if enable_height else 0.0, 0.0), Q15_MAX)
if height >= Q15_MAX:
point_gains = top
elif height > 0.0:
floor_weight, height_weight = equal_power_pair(height)
point_gains = floor * floor_weight + top * height_weight
internal = np.zeros(target.channel_count, dtype=np.float64)
gain = position_gain(target.speaker_bitfield, v, w) * float(object_gain)
for point_gain, speaker_id in zip(point_gains, region.speaker_ids):
internal[speaker_id] = point_gain * gain
if standard_order:
return internal[list(target.internal_to_standard)]
return internal
def align_metadata_sample(sample: int, block_size: int = 32) -> int:
return block_size * ((int(sample) + block_size // 2 - 1) // block_size)
def ramp_block_count(duration_samples: int, block_size: int = 32,
rate_scale: int = 1) -> int:
return (int(duration_samples) * int(rate_scale) + block_size // 2 - 1) // block_size
def _all_object_targets(layout: SpeakerLayout, state: JocFieldState) -> np.ndarray:
result = np.zeros((15, layout.channel_count), dtype=np.float64)
for object_index in range(1, 16):
result[object_index - 1] = render_point_gains(
layout,
state.q[(object_index, "q1")] / Q15_SCALE,
state.q[(object_index, "q2")] / Q15_SCALE,
state.q[(object_index, "q3")] / Q15_SCALE,
standard_order=False,
)
return result
class PythonSpeakerRenderer:
"""Stateful float64 speaker renderer with the same frame API as native."""
def __init__(self, layout: str | SpeakerLayout, block_size: int = 32):
self.layout = get_speaker_layout(layout) if isinstance(layout, str) else layout
self.block_size = int(block_size)
if self.block_size <= 0:
raise ValueError("block_size must be positive")
self.reset()
def reset(self):
channels = self.layout.channel_count
self._state = JocFieldState()
self._current = np.zeros((15, channels), dtype=np.float64)
self._target = np.zeros_like(self._current)
self._step = np.zeros_like(self._current)
self._remaining = np.zeros((15, channels), dtype=np.int32)
self._window = np.arange(self.block_size, dtype=np.float64) / float(self.block_size)
def _apply_payload(self, payload):
update = frame_update(payload)
self._state.apply(update["values"])
new_target = _all_object_targets(self.layout, self._state)
blocks = ramp_block_count(update["ramp_duration_samples"], self.block_size)
difference = new_target - self._current
significant = np.abs(difference) >= GAIN_SNAP_THRESHOLD
self._target[...] = new_target
self._current[~significant] = new_target[~significant]
self._step[~significant] = 0.0
self._remaining[~significant] = 0
if blocks:
self._step[significant] = difference[significant] / float(blocks)
self._remaining[significant] = blocks
else:
self._current[significant] = new_target[significant]
self._step[significant] = 0.0
self._remaining[significant] = 0
def process(self, objects16: np.ndarray, events=(), *, standard_order: bool = True,
output: np.ndarray | None = None) -> np.ndarray:
source = np.asarray(objects16)
if source.ndim != 2 or source.shape[1] != 16:
raise ValueError(f"objects16 must have shape [samples,16], got {source.shape}")
if len(source) % self.block_size:
raise ValueError(f"sample count must be divisible by {self.block_size}")
total_samples = len(source)
channels = self.layout.channel_count
if output is None:
output = np.zeros((total_samples, channels), dtype=np.float64)
elif output.shape != (total_samples, channels) or output.dtype != np.float64:
raise ValueError((output.shape, output.dtype))
else:
output[...] = 0.0
if "LFE" in self.layout.channels:
output[:, 3] = source[:, 0].astype(np.float64, copy=False)
events_by_block: dict[int, list[bytes]] = defaultdict(list)
for sample_offset, payload in events:
if not 0 <= int(sample_offset) <= total_samples:
raise ValueError(f"metadata offset outside process call: {sample_offset}")
block = align_metadata_sample(sample_offset, self.block_size) // self.block_size
events_by_block[block].append(bytes(payload))
total_blocks = total_samples // self.block_size
for block in range(total_blocks):
for payload in events_by_block.get(block, ()):
self._apply_payload(payload)
start = block * self.block_size
stop = start + self.block_size
out_block = output[start:stop]
for object_index in range(15):
active = self._remaining[object_index] > 0
gains = np.broadcast_to(self._target[object_index],
(self.block_size, channels)).copy()
if np.any(active):
gains[:, active] = (
self._current[object_index, active][None, :]
+ self._window[:, None] * self._step[object_index, active][None, :]
)
out_block += (
source[start:stop, object_index + 1, None].astype(np.float64, copy=False)
* gains
)
if np.any(active):
self._current[object_index, active] += self._step[object_index, active]
self._remaining[object_index, active] -= 1
finished = self._remaining[object_index] == 0
self._current[object_index, finished] = self._target[object_index, finished]
for payload in events_by_block.get(total_blocks, ()):
self._apply_payload(payload)
if standard_order:
return output[:, list(self.layout.internal_to_standard)]
return output
def render_frame(self, objects16, payload=None, metadata_offset=1473):
source = np.asarray(objects16)
if source.shape != (1536, 16):
raise ValueError(f"frame must have shape (1536,16), got {source.shape}")
events = () if payload is None else ((int(metadata_offset), bytes(payload)),)
return self.process(source, events)
def render_objects16(objects16: np.ndarray, payload_events: Iterable[tuple[int, bytes]],
layout: str | SpeakerLayout, *, block_size: int = 32,
standard_order: bool = True, output: np.ndarray | None = None) -> np.ndarray:
"""Render a complete interleaved LFE + 15 object stream."""
renderer = PythonSpeakerRenderer(layout, block_size=block_size)
return renderer.process(objects16, payload_events, standard_order=standard_order, output=output)
def payload_events_from_index(index, frame_count: int, *, frame_samples: int = 1536,
payload_id: int = 11, outer_offset_samples: int = 1473):
for frame, row in enumerate(index.rows[:frame_count]):
subpayloads = index.subpayloads(row)
if payload_id in subpayloads:
timing = frame_update(subpayloads[payload_id])
yield (
frame * frame_samples + outer_offset_samples + timing["block_offset_samples"],
subpayloads[payload_id],
)
+157
View File
@@ -0,0 +1,157 @@
"""Streaming spool and WAV writer for direct speaker-layout output."""
from __future__ import annotations
import struct
from pathlib import Path
import numpy as np
WAVE_FORMAT_PCM = 0x0001
WAVE_FORMAT_IEEE_FLOAT = 0x0003
WAVE_FORMAT_EXTENSIBLE = 0xFFFE
_PCM_GUID = bytes.fromhex("0100000000001000800000aa00389b71")
_FLOAT_GUID = bytes.fromhex("0300000000001000800000aa00389b71")
class SpeakerPcmSpool:
"""Temporary interleaved float32 store with float64 peak analysis."""
def __init__(self, path, sample_count, channel_count):
self.path = Path(path)
self.sample_count = int(sample_count)
self.channel_count = int(channel_count)
self.position = 0
self.peak = 0.0
self.clipped_values = 0
self.values = np.memmap(
self.path, dtype="<f4", mode="w+",
shape=(self.sample_count, self.channel_count),
)
def write_frame(self, pcm):
values = np.asarray(pcm, dtype=np.float64)
if values.ndim != 2 or values.shape[1] != self.channel_count:
raise ValueError(
f"speaker frame must have shape [samples,{self.channel_count}], got {values.shape}")
if self.position + len(values) > self.sample_count:
raise ValueError("speaker spool received more samples than allocated")
if not np.all(np.isfinite(values)):
raise ValueError("speaker renderer produced NaN or infinity")
absolute = np.abs(values)
if absolute.size:
self.peak = max(self.peak, float(np.max(absolute)))
self.clipped_values += int(np.count_nonzero(absolute > 1.0))
self.values[self.position:self.position + len(values)] = values.astype(np.float32)
self.position += len(values)
def finalize(self):
if self.position != self.sample_count:
raise ValueError(
f"speaker spool has {self.position} samples, expected {self.sample_count}")
self.values.flush()
return self
def close(self):
values = self.values
self.values = None
del values
def _fmt_chunk(channel_count, rate, sample_format):
if sample_format == "float32":
bits = 32
bytes_per_sample = 4
simple_tag = WAVE_FORMAT_IEEE_FLOAT
guid = _FLOAT_GUID
elif sample_format == "int24":
bits = 24
bytes_per_sample = 3
simple_tag = WAVE_FORMAT_PCM
guid = _PCM_GUID
else:
raise ValueError(f"unsupported speaker WAV format: {sample_format}")
block_align = channel_count * bytes_per_sample
byte_rate = rate * block_align
if channel_count <= 2:
body = struct.pack(
"<HHIIHH", simple_tag, channel_count, rate,
byte_rate, block_align, bits)
else:
body = (
struct.pack(
"<HHIIHHH", WAVE_FORMAT_EXTENSIBLE, channel_count, rate,
byte_rate, block_align, bits, 22)
+ struct.pack("<HI", bits, 0)
+ guid
)
return body, bits, bytes_per_sample, block_align
def _write_header(stream, channel_count, sample_count, rate, sample_format):
fmt, bits, bytes_per_sample, block_align = _fmt_chunk(
channel_count, rate, sample_format)
data_size = sample_count * block_align
riff_file_size = 12 + 8 + len(fmt) + 8 + data_size
use_rf64 = riff_file_size - 8 > 0xFFFFFFFF
if use_rf64:
# RF64 + ds64 + fmt + data.
file_size = 12 + 36 + 8 + len(fmt) + 8 + data_size
stream.write(b"RF64")
stream.write(struct.pack("<I", 0xFFFFFFFF))
stream.write(b"WAVE")
stream.write(b"ds64")
stream.write(struct.pack("<IQQQI", 28, file_size - 8, data_size, sample_count, 0))
else:
stream.write(b"RIFF")
stream.write(struct.pack("<I", riff_file_size - 8))
stream.write(b"WAVE")
stream.write(b"fmt ")
stream.write(struct.pack("<I", len(fmt)))
stream.write(fmt)
stream.write(b"data")
stream.write(struct.pack("<I", 0xFFFFFFFF if use_rf64 else data_size))
return {
"format": sample_format,
"bits_per_sample": bits,
"bytes_per_sample": bytes_per_sample,
"block_align": block_align,
"data_bytes": data_size,
"rf64": use_rf64,
}
def _pack_int24(values):
scaled = (np.clip(values, -1.0, 1.0) * np.float32(8388607.0)).astype(np.int32)
unsigned = scaled.reshape(-1).view(np.uint32)
packed = np.empty((unsigned.size, 3), dtype=np.uint8)
packed[:, 0] = unsigned & 0xFF
packed[:, 1] = (unsigned >> 8) & 0xFF
packed[:, 2] = (unsigned >> 16) & 0xFF
return packed.tobytes()
def write_speaker_wav(path, pcm, sample_format, *, rate=48000, chunk_samples=262144):
"""Write an interleaved float32 array/memmap as float32 or PCM24 WAV."""
target = Path(path)
values = np.asarray(pcm)
if values.ndim != 2:
raise ValueError(f"speaker PCM must be 2D, got {values.shape}")
sample_count, channel_count = values.shape
target.parent.mkdir(parents=True, exist_ok=True)
with target.open("wb") as stream:
info = _write_header(
stream, channel_count, sample_count, int(rate), sample_format)
for start in range(0, sample_count, int(chunk_samples)):
block = np.asarray(values[start:start + chunk_samples], dtype="<f4")
if sample_format == "float32":
stream.write(block.tobytes(order="C"))
else:
stream.write(_pack_int24(block))
info.update({
"path": str(target.resolve()),
"sample_rate": int(rate),
"sample_count": int(sample_count),
"channel_count": int(channel_count),
"file_bytes": target.stat().st_size,
})
return info
+65
View File
@@ -0,0 +1,65 @@
"""为未覆盖的 E-AC-3 JOC/EMDF 变体生成结构化错误和维修报告。"""
import hashlib
import json
from pathlib import Path
def bytes_descriptor(data):
"""返回足以识别载荷、但不会复制整段载荷的摘要。"""
raw = bytes(data)
return {
"bytes": len(raw),
"sha256": hashlib.sha256(raw).hexdigest(),
"prefix_hex": raw[:32].hex(),
"suffix_hex": raw[-16:].hex() if raw else "",
}
class UnsupportedVariantError(ValueError):
"""表示输入结构有效或疑似有效,但当前解析器没有覆盖该变体。"""
def __init__(self, stage, variant, message, *, frame=None, details=None):
self.stage = str(stage)
self.variant = str(variant)
self.message = str(message)
self.frame = frame
self.details = dict(details or {})
super().__init__(self.__str__())
def add_context(self, *, frame=None, details=None):
if self.frame is None and frame is not None:
self.frame = int(frame)
if details:
for key, value in details.items():
self.details.setdefault(key, value)
self.args = (self.__str__(),)
return self
def to_dict(self):
return {
"report_schema": 1,
"error": "unsupported_variant",
"stage": self.stage,
"variant": self.variant,
"frame": self.frame,
"message": self.message,
"details": self.details,
}
def __str__(self):
where = f" frame={self.frame}" if self.frame is not None else ""
detail = json.dumps(self.details, ensure_ascii=False, separators=(",", ":"))
return f"[{self.stage}/{self.variant}]{where} {self.message}; details={detail}"
def write_variant_report(path, error, *, input_path=None, output_path=None):
"""将变体错误写成可交给后续维修工具的 JSON。"""
report = error.to_dict()
if input_path is not None:
report["input"] = str(Path(input_path))
if output_path is not None:
report["requested_output"] = str(Path(output_path))
target = Path(path)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
return target