commit 6536782a0fb334e01560722c5eaf075c8c8d98a9 Author: TheM14 Date: Tue Sep 1 16:31:38 2026 +0800 Initial public release of JustOneCacophony diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae17595 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +__pycache__/ +*.py[cod] + +.venv/ +venv/ + +build/ +output/ +metadata_cache/ + +*.metadata.json +*.report.json +*.variant-error.json +*.objects16.f32le + diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..30022e3 --- /dev/null +++ b/README.en.md @@ -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) diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f1c0b4 --- /dev/null +++ b/README.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) diff --git a/data/README.en.md b/data/README.en.md new file mode 100644 index 0000000..75db08c --- /dev/null +++ b/data/README.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. diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..085106b --- /dev/null +++ b/data/README.md @@ -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`。修改任何一侧时,应同步更新另一侧并进行逐值一致性检查。 diff --git a/data/tables.npz b/data/tables.npz new file mode 100644 index 0000000..6b49560 Binary files /dev/null and b/data/tables.npz differ diff --git a/docs/math.en.md b/docs/math.en.md new file mode 100644 index 0000000..f9239e8 --- /dev/null +++ b/docs/math.en.md @@ -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_00. +$$ + +### 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[2][10][64]`; +- Ls/Rs band-0 FIR history: `complex[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. diff --git a/docs/native.md b/docs/native.md new file mode 100644 index 0000000..703ca28 --- /dev/null +++ b/docs/native.md @@ -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[2][10][64]`; +- Ls/Rs band-0 FIR history:`complex[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)。 diff --git a/main.py b/main.py new file mode 100644 index 0000000..951cda7 --- /dev/null +++ b/main.py @@ -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) diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt new file mode 100644 index 0000000..73eeff8 --- /dev/null +++ b/native/CMakeLists.txt @@ -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$<$:Debug>") + target_compile_options(eac3joc_core PRIVATE + /W4 /permissive- /Zc:__cplusplus /utf-8 /EHsc /GR- /fp:precise + $<$:/O2> + $<$:/Oi> + $<$:/GL> + ) + target_link_options(eac3joc_core PRIVATE + $<$:/LTCG> + /INCREMENTAL:NO /OPT:REF /OPT:ICF + ) +else() + target_compile_options(eac3joc_core PRIVATE + -Wall -Wextra -Wpedantic -fno-fast-math + $<$:-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=/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 "$" + DESTINATION . + OPTIONAL + ) +endif() + diff --git a/native/include/eac3joc_core.h b/native/include/eac3joc_core.h new file mode 100644 index 0000000..10caecc --- /dev/null +++ b/native/include/eac3joc_core.h @@ -0,0 +1,119 @@ +#pragma once + +#include + +#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 diff --git a/native/src/eac3joc_core.cpp b/native/src/eac3joc_core.cpp new file mode 100644 index 0000000..fb3712d --- /dev/null +++ b/native/src/eac3joc_core.cpp @@ -0,0 +1,735 @@ +#define EJOC_BUILD_DLL +#include "eac3joc_core.h" +#include "qmf_tables.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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>(static_cast(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(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(reversed); + const double theta = kPi * static_cast(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(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(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(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((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(static_cast(frame_sample) * phase_step); + gain = static_cast(phase_old + product); + } + scaled[sample] = static_cast(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((((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(timeslot + 1) / 24.0; + coefficient = previous * (1.0 - alpha) + target0 * alpha; + } else if (timeslot < 12) { + const double alpha = static_cast(timeslot + 1) / 12.0; + coefficient = previous * (1.0 - alpha) + target0 * alpha; + } else { + const double alpha = static_cast(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(clipped * clipgain); + destination[timeslot * 64 + sample] = static_cast(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(clamp_unit(lfe_delay_[sample])); + output16[sample] = static_cast(value * output_scale); + } + for (int sample = kLfeDelay; sample < 1536; ++sample) { + const float value = static_cast(clamp_unit(static_cast(lfe[sample - kLfeDelay]))); + output16[sample] = static_cast(value * output_scale); + } + for (int sample = 0; sample < kLfeDelay; ++sample) { + lfe_delay_[sample] = static_cast(lfe[sample + (1536 - kLfeDelay)]); + } + } + + enum class JobKind : uint8_t { Analysis, Objects }; + + std::vector workers_; + std::unique_ptr> work_barrier_; + std::atomic stop_{false}; + std::atomic pool_ready_{false}; + std::atomic 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(handle); +} + +int EJOC_CALL ejoc_renderer_reset(ejoc_renderer_handle handle) { + if (!handle) { + return -1; + } + return static_cast(handle)->reset(); +} + +int EJOC_CALL ejoc_renderer_set_threads(ejoc_renderer_handle handle, uint32_t total_threads) { + if (!handle) { + return -1; + } + return static_cast(handle)->set_threads(total_threads); +} + +uint32_t EJOC_CALL ejoc_renderer_thread_count(ejoc_renderer_handle handle) { + if (!handle) { + return 0; + } + return static_cast(handle)->thread_count(); +} + +const char* EJOC_CALL ejoc_renderer_last_error(ejoc_renderer_handle handle) { + if (!handle) { + return "null renderer handle"; + } + return static_cast(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(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" diff --git a/native/src/joc_huffman_tables.h b/native/src/joc_huffman_tables.h new file mode 100644 index 0000000..c3911a6 --- /dev/null +++ b/native/src/joc_huffman_tables.h @@ -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} +}; diff --git a/native/src/qmf_tables.h b/native/src/qmf_tables.h new file mode 100644 index 0000000..e63f0e3 --- /dev/null +++ b/native/src/qmf_tables.h @@ -0,0 +1,388 @@ +#pragma once + +#include + +// 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 diff --git a/native/src/speaker_layouts.h b/native/src/speaker_layouts.h new file mode 100644 index 0000000..cbd7f89 --- /dev/null +++ b/native/src/speaker_layouts.h @@ -0,0 +1,534 @@ +#pragma once + +#include +#include +#include + +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 coordinate_q15{}; + std::uint8_t speaker_id{}; +}; + +struct AxisGroup { + std::uint8_t size{}; + std::array 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 points{}; + std::array axis0_groups{}; + std::array axis1_groups{}; +}; + +struct LayoutGeometry { + std::uint32_t speaker_bitfield{}; + std::uint8_t out_ch_config{}; + std::uint8_t channel_count{}; + std::array standard_from_internal{}; + std::array regions{}; +}; + +inline constexpr std::array 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 diff --git a/native/src/speaker_renderer.cpp b/native/src/speaker_renderer.cpp new file mode 100644 index 0000000..385cce5 --- /dev/null +++ b/native/src/speaker_renderer.cpp @@ -0,0 +1,495 @@ +#define EJOC_BUILD_DLL +#include "eac3joc_core.h" +#include "speaker_layouts.h" + +#include +#include +#include +#include +#include +#include +#include + +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; +using ChannelGains = std::array; +using ObjectChannelGains = std::array; +using RemainingCounts = std::array, 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(region.points[point].coordinate_q15[component]) / kQ15Scale; +} + +std::uint64_t expand_speaker_bitfield(const std::uint32_t compact) noexcept { + constexpr std::array 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((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(height_channels) / 4.0, 1.0); + const double floor_factor = std::min(static_cast(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(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& 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& 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& 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(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(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(sample) + kBlock / 2 - 1) / kBlock; + } + + static std::uint32_t ramp_blocks(const std::uint32_t duration) noexcept { + return static_cast( + (static_cast(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(position[0]) / kQ15Scale; + const double v = static_cast(position[1]) / kQ15Scale; + const double w = static_cast(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(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(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(sample) / static_cast(kBlock)) * + step_[object][internal] + : fixed_gain; + output[(start + sample) * layout_->channel_count + standard] += + static_cast( + 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 standard_index_for_internal_{}; + std::array 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(handle); +} + +int EJOC_CALL ejoc_speaker_renderer_reset(ejoc_speaker_renderer_handle handle) { + if (!handle) { + return -1; + } + return static_cast(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(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(handle)->process( + objects16_interleaved, sample_count, metadata_count, + metadata_offsets, ramp_durations, positions_q15, + region_indices, height_enabled, object_gains, output_interleaved); +} + +} // extern "C" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9f161ac --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +numpy>=1.24 diff --git a/src/adm_assemble.py b/src/adm_assemble.py new file mode 100644 index 0000000..fbd8e38 --- /dev/null +++ b/src/adm_assemble.py @@ -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 diff --git a/src/adm_atmos.py b/src/adm_atmos.py new file mode 100644 index 0000000..9d52997 --- /dev/null +++ b/src/adm_atmos.py @@ -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("= 0: + self.fp.seek(m + 4); self.fp.write(struct.pack("= 0: + self.fp.seek(m + 8) + self.fp.write(struct.pack(" {out_path} ({duration_sec:.2f}s, 25ch, axml={len(axml)}B, " + f"chna={len(chna)}B, dbmd={len(dbmd)}B)") diff --git a/src/adm_serializer.py b/src/adm_serializer.py new file mode 100644 index 0000000..485420c --- /dev/null +++ b/src/adm_serializer.py @@ -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('') + a('') + a('') + a(f'') + a('ACO_1001') + a('ACO_1002') + a('') + a('') + a('AO_1001') + a('2') + a('') + a('') + for i in range(N_OBJ): + a(f'AO_{0x100b + i:04x}') + a('2') + a('') + a(f'') + a('AP_00011001') + for i in range(10): + a(f'ATU_{i + 1:08x}') + a('') + for i in range(N_OBJ): + a(f'') + a(f'AP_0003{0x1001 + i:04x}') + a(f'ATU_{11 + i:08x}') + a('') + a('') + for i in range(10): + a(f'AC_0001{0x1001 + i:04x}') + a('') + for i in range(N_OBJ): + a(f'') + a(f'AC_0003{0x1001 + i:04x}') + a('') + for i in range(10): + a(f'') + a(f'') + a('1') + x, y, z = BED_POS[i] + a(f'{x:.10f}') + a(f'{y:.10f}') + if z != 0: + a(f'{z:.10f}') + a(f'{BED_LABELS[i]}') + a('') + a('') + for i, (oname, kfs) in enumerate(obj_tracks): + a(f'') + 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'') + a('1') + a(f'{x:.10f}') + a(f'{y:.10f}') + if z != 0: + a(f'{z:.10f}') + a(f'1') + a('') + a('') + for i in range(10): + a(f'') + a(f'AT_0001{0x1001 + i:04x}_01') + a('AP_00011001') + a('') + for i in range(N_OBJ): + a(f'') + a(f'AT_0003{0x1001 + i:04x}_01') + a(f'AP_0003{0x1001 + i:04x}') + a('') + for i in range(10): + a(f'') + a(f'AS_0001{0x1001 + i:04x}') + a('') + for i in range(N_OBJ): + a(f'') + a(f'AS_0003{0x1001 + i:04x}') + a('') + for i in range(10): + a(f'') + a(f'AC_0001{0x1001 + i:04x}') + a('AP_00011001') + a(f'AT_0001{0x1001 + i:04x}_01') + a('') + for i in range(N_OBJ): + a(f'') + a(f'AC_0003{0x1001 + i:04x}') + a(f'AP_0003{0x1001 + i:04x}') + a(f'AT_0003{0x1001 + i:04x}_01') + a('') + a('') + a('') + return ''.join(w).encode('utf-8') diff --git a/src/adm_validate.py b/src/adm_validate.py new file mode 100644 index 0000000..aeba821 --- /dev/null +++ b/src/adm_validate.py @@ -0,0 +1,199 @@ +"""校验 ADM BWF 的 RF64、通道、axml、chna、dbmd 和对象引用结构。 + +用法:``python src/adm_validate.py [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(" 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(" {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" 1 + else "ADM does not have a required audioProgramme object") + if "]*typeLabel="0003".*?', 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']*rtime="[^"]+".*?', + 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'1', 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(" 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 diff --git a/src/evo_unpack.py b/src/evo_unpack.py new file mode 100644 index 0000000..9788713 --- /dev/null +++ b/src/evo_unpack.py @@ -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 diff --git a/src/joc_decode.py b/src/joc_decode.py new file mode 100644 index 0000000..6216f70 --- /dev/null +++ b/src/joc_decode.py @@ -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 diff --git a/src/joc_qmf.py b/src/joc_qmf.py new file mode 100644 index 0000000..77e6cc6 --- /dev/null +++ b/src/joc_qmf.py @@ -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 diff --git a/src/metadata.py b/src/metadata.py new file mode 100644 index 0000000..534f077 --- /dev/null +++ b/src/metadata.py @@ -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 diff --git a/src/native_renderer.py b/src/native_renderer.py new file mode 100644 index 0000000..c964831 --- /dev/null +++ b/src/native_renderer.py @@ -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) diff --git a/src/oamd_bits.py b/src/oamd_bits.py new file mode 100644 index 0000000..efa3e38 --- /dev/null +++ b/src/oamd_bits.py @@ -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) diff --git a/src/oamd_tracks.py b/src/oamd_tracks.py new file mode 100644 index 0000000..56e75e4 --- /dev/null +++ b/src/oamd_tracks.py @@ -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) + ] diff --git a/src/renderer.py b/src/renderer.py new file mode 100644 index 0000000..540dd7e --- /dev/null +++ b/src/renderer.py @@ -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 + + diff --git a/src/speaker_backend.py b/src/speaker_backend.py new file mode 100644 index 0000000..07cfc19 --- /dev/null +++ b/src/speaker_backend.py @@ -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 + diff --git a/src/speaker_layouts.py b/src/speaker_layouts.py new file mode 100644 index 0000000..fa6e236 --- /dev/null +++ b/src/speaker_layouts.py @@ -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] diff --git a/src/speaker_native_renderer.py b/src/speaker_native_renderer.py new file mode 100644 index 0000000..ea13c52 --- /dev/null +++ b/src/speaker_native_renderer.py @@ -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) diff --git a/src/speaker_renderer.py b/src/speaker_renderer.py new file mode 100644 index 0000000..209642f --- /dev/null +++ b/src/speaker_renderer.py @@ -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], + ) diff --git a/src/speaker_wav.py b/src/speaker_wav.py new file mode 100644 index 0000000..97a6c8d --- /dev/null +++ b/src/speaker_wav.py @@ -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=" 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( + " 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("> 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="