Initial public release of JustOneCacophony
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
build/
|
||||
output/
|
||||
metadata_cache/
|
||||
|
||||
*.metadata.json
|
||||
*.report.json
|
||||
*.variant-error.json
|
||||
*.objects16.f32le
|
||||
|
||||
+167
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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`。修改任何一侧时,应同步更新另一侧并进行逐值一致性检查。
|
||||
Binary file not shown.
+578
@@ -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
@@ -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 属于独立后处理,不包含在上述混音公式中。
|
||||
@@ -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
@@ -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)。
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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}
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
numpy>=1.24
|
||||
@@ -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
|
||||
@@ -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)")
|
||||
@@ -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("&", "&").replace("<", "<").replace(">", ">"))
|
||||
|
||||
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')
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
@@ -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],
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user