@@ -0,0 +1,43 @@
|
||||
name: build
|
||||
|
||||
# Builds both architectures and publishes the two installable component packages.
|
||||
# The SDK is fetched from foobar2000.org at build time (tools/setup_sdk.ps1) and the
|
||||
# renderer sources are part of this repository (kernel/), so nothing else is needed.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch the foobar2000 SDK (target 1.5/1.6)
|
||||
shell: pwsh
|
||||
run: ./tools/setup_sdk.ps1 -TargetVersion 80
|
||||
|
||||
- name: Build both architectures and package them
|
||||
shell: pwsh
|
||||
run: ./tools/package.ps1
|
||||
|
||||
- name: List the packages
|
||||
shell: pwsh
|
||||
run: Get-ChildItem dist -Filter *.fb2k-component | Select-Object Name, Length
|
||||
|
||||
- name: Upload the x86 component (foobar2000 1.6 and 2.x 32-bit)
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: foo_input_joc-x86
|
||||
path: dist/*-x86.fb2k-component
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload the x64 component (foobar2000 2.x 64-bit)
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: foo_input_joc-x64
|
||||
path: dist/*-x64.fb2k-component
|
||||
if-no-files-found: error
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Build products
|
||||
/build/
|
||||
/dist/
|
||||
/SDK/
|
||||
|
||||
# Visual Studio
|
||||
.vs/
|
||||
*.user
|
||||
*.suo
|
||||
*.aps
|
||||
*.tlog
|
||||
*.idb
|
||||
*.ilk
|
||||
*.pdb
|
||||
*.obj
|
||||
*.exp
|
||||
|
||||
# Component binaries: only ever produced by a build, never committed
|
||||
*.dll
|
||||
*.lib
|
||||
*.fb2k-component
|
||||
|
||||
# HRTF data is supplied by whoever runs the component and is never distributed:
|
||||
# a SOFA measurement set or a personalised headphone model must not be committed.
|
||||
*.sofa
|
||||
*.jochrtf
|
||||
*.personalized_headphone
|
||||
rosella_kernels.npz
|
||||
|
||||
# The component writes its own log next to the DLL
|
||||
joc_decoder.log
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 TheM14
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,114 @@
|
||||
# foo_input_joc
|
||||
|
||||
foobar2000 input component for **E-AC-3 JOC (Dolby Atmos)** files: the JOC objects are
|
||||
rendered to binaural (HRTF) or to a speaker layout up to 7.1, in real time.
|
||||
|
||||
Two files with the same name pay for the whole thing: `joc_core`'s C++ sources are copied
|
||||
into [`kernel/`](kernel/) and compiled straight into the component, so there is nothing to
|
||||
install beside `foo_input_joc.dll`.
|
||||
|
||||
## What it does
|
||||
|
||||
1. Reads the E-AC-3 syncframes and decides from the bitstream whether the file really
|
||||
carries JOC (an EMDF container holding both the OAMD and the JOC payload).
|
||||
2. A file without JOC is handed back to foobar2000 with `exception_io_unsupported_format`,
|
||||
so the built-in E-AC-3 decoder plays it — this component never decodes plain E-AC-3.
|
||||
3. A JOC file is decoded as: the syncframes go to the renderer as metadata, the 5.1 core
|
||||
PCM comes from ffmpeg, and the renderer pairs them (one syncframe : 1536 bed samples)
|
||||
and produces the output PCM, which is handed back to foobar2000.
|
||||
|
||||
```
|
||||
.eac3 file
|
||||
├─ JOC check (src/eac3_scan.cpp) ─── no JOC ──▶ built-in E-AC-3 decoder
|
||||
└─ JOC
|
||||
├─ syncframes ────────────────────▶ renderer metadata
|
||||
└─ ffmpeg -ac 6 -c:a pcm_f32le ───▶ 5.1 core PCM ──▶ renderer bed
|
||||
│
|
||||
▼
|
||||
2 ch or ≤7.1 PCM ──▶ foobar2000
|
||||
```
|
||||
|
||||
## Repository layout
|
||||
|
||||
| Path | Contents |
|
||||
|---|---|
|
||||
| `kernel/` | Copy of the `joc_core` C++ sources (`include/` + `src/`) and `joc_kernel.vcxproj`, the static library the component links |
|
||||
| `src/eac3_scan.*` | Syncframe walk and the JOC bitstream test |
|
||||
| `src/joc_decode.*` | Decode engine: starts ffmpeg, drives the renderer, handles the end of stream. No foobar2000 headers, so it also builds into the offline tools |
|
||||
| `src/input_joc.cpp` | The foobar2000 input: format recognition, yielding, `get_info`, `initialize`, `run` |
|
||||
| `src/settings.*` | Configuration values and their environment overrides (development only) |
|
||||
| `src/prefs.cpp`, `src/prefs.rc` | The preferences page |
|
||||
| `src/log.*` | Diagnostic log written next to the DLL |
|
||||
| `tests/` | Offline tools: bitstream self-test and cross-check against the renderer, render harness, preferences-page layout check |
|
||||
| `tools/` | SDK fetch, build, package, deploy, unattended test bed run |
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
pwsh -File tools/setup_sdk.ps1 # official SDK into SDK/, pinned to target 1.5/1.6
|
||||
pwsh -File tools/build.ps1 # Win32 -> build\Win32\foo_input_joc.dll
|
||||
pwsh -File tools/build.ps1 -Platform x64
|
||||
pwsh -File tools/package.ps1 # both, packaged into dist\*.fb2k-component
|
||||
```
|
||||
|
||||
`Release-Static` uses the static CRT (`/MT`); `/fp:precise` is required and must not be
|
||||
changed. `foo_input_joc.vcxproj` builds `kernel\joc_kernel.vcxproj` first through a project
|
||||
reference. The copied kernel sources are compiled with `JOC_STATIC` / `EJOC_STATIC` so their
|
||||
entry points are neither imported nor exported.
|
||||
|
||||
## Install
|
||||
|
||||
Either drop `dist\foo_input_joc-<version>-<arch>.fb2k-component` onto foobar2000 (or use
|
||||
Preferences → Components → Install), or copy `foo_input_joc.dll` into
|
||||
`<profile>\user-components\foo_input_joc\`. The per-component subdirectory is required:
|
||||
a DLL lying directly in `user-components\` is not scanned. 1.6 is 32-bit, 2.x ships both,
|
||||
and a DLL of the wrong architecture is silently ignored.
|
||||
|
||||
`tools/deploy.ps1 -TestBed <path to portable foobar2000>` does the manual variant, and
|
||||
`tools/run.ps1 -TestBed <path> -Play <file>` runs it unattended and prints the log.
|
||||
Always let foobar2000 exit through `/exit`; a force-killed instance leaves a
|
||||
`<profile>\running` marker behind and the next start then refuses to load any user
|
||||
component.
|
||||
|
||||
## Settings
|
||||
|
||||
Preferences → Tools → **JOC decoder**:
|
||||
|
||||
* **Output** — binaural, or a speaker layout from 2.0 to 7.1;
|
||||
* **Binaural mode** (near / mid / far) and the room **tail** in seconds;
|
||||
* **HRTF source** — a **SOFA** file or a **Rosella** `.personalized_headphone` model. Leave
|
||||
the path empty to use the default location `<component directory>\HRTF\`:
|
||||
`binaural.sofa` or `binaural.personalized_headphone`;
|
||||
* **Gain** — a switch plus a value in dB. Binaural rendering can exceed full scale on
|
||||
material that does not clip in the core mix, so attenuation belongs here;
|
||||
* the **ffmpeg** executable to use.
|
||||
|
||||
Nothing on the page is disabled; the status line states what is in effect.
|
||||
|
||||
**HRTF data is not distributed with this repository.** A SOFA measurement set or a
|
||||
personalised headphone model is supplied by whoever runs the component (and is listed in
|
||||
`.gitignore` so it cannot be committed by accident). Speaker layouts and every offline test
|
||||
except binaural rendering work without one; binaural rendering without an HRTF fails with a
|
||||
message naming the file it looked for.
|
||||
|
||||
## Environment overrides
|
||||
|
||||
Development only: they override the stored settings for one run and every use is logged.
|
||||
`JOC_OUTPUT`, `JOC_LAYOUT`, `JOC_HRTF`, `JOC_HRTF_SOURCE`, `JOC_BINAURAL_MODE`, `JOC_GAIN_DB`,
|
||||
`JOC_GAIN_ENABLED`, `JOC_TAIL_SECONDS`, `JOC_OBJECT_DELAY`, `JOC_THREADS`, `JOC_FFMPEG`,
|
||||
`JOC_LOG`.
|
||||
|
||||
## Known limitations
|
||||
|
||||
* ADM BWF output is not implemented.
|
||||
* Containers (`.m4a`, `.mkv`) are not claimed: only bare `.eac3` / `.ec3` streams.
|
||||
* The room tail is returned in full; the reference command-line renderer additionally trims
|
||||
trailing samples below a threshold, so its output can be shorter.
|
||||
* x86 and x64 do not produce bit-identical binaural output (last-bit differences): the
|
||||
renderer's SIMD dispatch only applies to x86-64/ARM64, so 32-bit builds take the scalar
|
||||
path. The speaker path is bit-identical on both.
|
||||
|
||||
## Licence
|
||||
|
||||
`LICENSE` is the upstream MIT licence, copied unchanged; `kernel/` is a copy of the upstream
|
||||
renderer sources and keeps their notices. See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
@@ -0,0 +1,62 @@
|
||||
# Third-party notices / 第三方通知
|
||||
|
||||
本仓库包含 JustOneCacophony 原生库的逐字节副本(下称"复用文件"),以及遵循公开标准实现的
|
||||
滤波器组表。本文件记录这些来源、公开标准依据与权利边界。
|
||||
|
||||
## 复用文件
|
||||
|
||||
以下文件是本项目(JustOneCacophony,MIT)原生 C++ 库的逐字节副本,不修改、不追加注释:
|
||||
|
||||
| 本仓库路径 | 上游路径 | SHA-256(前 16 位) |
|
||||
|---|---|---|
|
||||
| `src/joc_core/eac3joc_core.cpp` | `native/src/eac3joc_core.cpp` | `1978eea64a2616fa` |
|
||||
| `src/joc_core/qmf_tables.h` | `native/src/qmf_tables.h` | `c205ea187e956e87` |
|
||||
| `src/speaker/speaker_renderer.cpp` | `native/src/speaker_renderer.cpp` | `96f36f40daf86eec` |
|
||||
| `src/speaker/speaker_layouts.h` | `native/src/speaker_layouts.h` | `51e24c11be09787f` |
|
||||
| `src/binaural/binaural_renderer.cpp` | `native/src/binaural_renderer.cpp` | `d579803f0f5a6699` |
|
||||
| `src/joc_bitstream/joc_huffman_tables.h` | `native/src/joc_huffman_tables.h` | `698498b3778d88db` |
|
||||
| `include/eac3joc_core.h` | `native/include/eac3joc_core.h` | `7392f48dfd840656` |
|
||||
|
||||
上游修订:`6bc2c2885666bb151bb66af93472199af9a99b81`。
|
||||
|
||||
## 派生文件
|
||||
|
||||
`src/binaural/sofa_binaural_renderer.cpp` 是上游 `native/src/sofa_binaural_renderer.cpp` 的派生
|
||||
实现:同一 `ejoc_sofa_binaural_*` C ABI,增加表提升、结果记忆化、输入校验与 SIMD 派发。它
|
||||
**不属于**逐字节副本,也不受"不得修改"约束,但来源固定为上述上游修订,对应上游源文件的
|
||||
SHA-256 为 `81b485e4c71907672ab308ddc382284acf29161cee93d7906785a4cce58941c7`,输出与原
|
||||
实现逐位相同(见该文件头部的验证记录)。
|
||||
|
||||
## 公开标准来源
|
||||
|
||||
64-QMF → 77-hybrid 结构与 13-tap 低带 prototype 定义于
|
||||
[3GPP TS 26.405 / ETSI TS 126 405](https://www.etsi.org/deliver/etsi_ts/126400_126499/126405/06.00.00_60/ts_126405v060000p.pdf)
|
||||
第 5.2.2 节(Table 1 的 $Q=8$/$Q=4$ 系数,delay 6):
|
||||
|
||||
$$G_q^p[n] = g^p[n]\cdot\exp\Bigl(j\,\frac{2\pi}{Q^p}\bigl(q+\tfrac12\bigr)(n-6)\Bigr)$$
|
||||
|
||||
64-band QMF analysis 即 ISO/IEC 14496-3/AMD1:2003 第 4.B.18.2 节的 MPEG-4
|
||||
AAC/SBR 64 complex QMF bank;打包的 $64\times10$ 表是公开 640-tap prototype 的多相重排:
|
||||
|
||||
$$A_{r,t} = \frac{(-1)^t}{128}\,c_{63-r+64t}$$
|
||||
|
||||
QMF synthesis 表为 analysis 多相矩阵 $\mathbf{A}$ 的因果左逆
|
||||
$\mathbf{A}\,\mathbf{W}=\mathbf{P}$($\mathbf{P}$ 为 577-sample 延迟置换;
|
||||
全链 $961 = 577 + 6\times64$),rank-4 分解存储:
|
||||
|
||||
$$W_{b,l} = \sum_{r=1}^{4} t_{b,l,r}\,\mathbf{b}_{b,r}^{\top}$$
|
||||
|
||||
hybrid synthesis 表为 77→64 重组:高频带恒等 $Y_{3+b}=X_{16+b}$,低频带:
|
||||
|
||||
$$Y_p = \sum_{q\in C_p}\Bigl(\mathrm{Re}X_q + j\,s_q\,\mathrm{Im}X_q\Bigr),\qquad s_q\in\{\pm1\}$$
|
||||
|
||||
相同数值可在 FFmpeg(`aacps_tablegen.h`、`aacsbrdata.h`)等公开实现中查到。
|
||||
|
||||
## HRTF 数据与 `.jochrtf`
|
||||
|
||||
`.jochrtf` 含有特定源 SOFA/HRTF 数据集的变换系数与 delay;其使用、复制与再分发仍受源
|
||||
数据集许可约束,权限不明确时应作为私有 cache 保存。本仓库不分发任何 HRTF 数据集。
|
||||
|
||||
## 专利说明
|
||||
|
||||
标准可公开获取不等于获准实施相关专利。
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
# Verification
|
||||
|
||||
Measured results only. Each entry names the command or the log line it came from, so it can
|
||||
be reproduced. Environment: Windows x64 host, official **foobar2000 1.6.19 x86** portable
|
||||
installation, official SDK 2026-09-17 pinned to `FOOBAR2000_TARGET_VERSION 80`, MSVC 14.44,
|
||||
ffmpeg 8.0.
|
||||
|
||||
Test material is supplied locally and is **not** part of this repository: the `testdata/` and
|
||||
`vectors/` files of the upstream renderer project, and — for the binaural measurements — an
|
||||
HRTF file. Everything except binaural rendering runs without any HRTF; binaural runs take the
|
||||
file as an argument (`tests/render_harness.cpp --hrtf …`) or use the default location beside
|
||||
the DLL.
|
||||
|
||||
## Component and renderer
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Sources compiled in, nothing loaded at run time | log: `core: in-process renderer 0.1.0-m1 (abi 3), component built against abi 3` |
|
||||
| One artefact, no companion DLL | `dist\*.fb2k-component` holds `foo_input_joc.dll` and `README.md` only |
|
||||
| Kernel sources untouched | the upstream working tree's file timestamps are unchanged; it is only ever read |
|
||||
| Both architectures build | `build\Win32\foo_input_joc.dll`, `build\x64\foo_input_joc.dll` |
|
||||
|
||||
## Playback in foobar2000 1.6.19 x86
|
||||
|
||||
| Case | Log evidence |
|
||||
|---|---|
|
||||
| Speaker 7.1, 5 s file | `stream created, 8 output channel(s), layout=7.1` … `frames_in=157 frames_out=157 samples_out=241152` — 157 × 1536 exactly |
|
||||
| Binaural, SOFA | `stream created, 2 output channel(s)` … `end of stream after 481215 frames` (241152 source + 240063 tail) |
|
||||
| Binaural, Rosella model | `stream created, 2 output channel(s)` … `end of stream after 481855 frames` |
|
||||
| Binaural, HRTF path left empty | resolves to `<component directory>\HRTF\binaural.sofa` and produces the same 481215 frames as naming that file explicitly |
|
||||
| Full 238 s file, binaural SOFA | `eac3 frames queued=7436, bed frames pushed=7436` … `samples_out=11661759`; no ffmpeg process left behind |
|
||||
| Installed from the `.fb2k-component` package | unpacked into `user-components\foo_input_joc\`, plays with the default-folder HRTF |
|
||||
|
||||
## Bitstream recognition
|
||||
|
||||
`tests/scan_crosscheck.cpp` compares, frame by frame, the syncframe lengths and the JOC
|
||||
verdict this component computes against the renderer's own `joc_eac3_frame_bytes()` /
|
||||
`joc_parse_eac3_frame()`:
|
||||
|
||||
```
|
||||
testdata\gold_forever.eac3 frames=64 plugin_joc=64 kernel_joc=64 len_mismatch=0 verdict_mismatch=0
|
||||
vectors\valid.eac3 frames=7 plugin_joc=7 kernel_joc=7 len_mismatch=0 verdict_mismatch=0
|
||||
build\plain_eac3.eac3 frames=64 plugin_joc=0 kernel_joc=0 len_mismatch=0 verdict_mismatch=0
|
||||
crosscheck: 3 file(s), AGREES WITH CORE
|
||||
```
|
||||
|
||||
Ten corrupt vectors were compared as well: no frame-length disagreement, verdicts agreed on
|
||||
9 of 10. The one difference is `corrupt_truncated_huffman.eac3`: this component only tests
|
||||
for the container, while the renderer also parses the payload and reports a truncated
|
||||
bitstream later.
|
||||
|
||||
## Plain E-AC-3 is handed back
|
||||
|
||||
Playing a file the component's own encoder produced without JOC:
|
||||
|
||||
```
|
||||
decoder: open "...plain_eac3.eac3" reason=1 bytes=144384 frames=8 with_joc=0
|
||||
decoder: yielding to the built-in decoder (at least one examined syncframe has no JOC EMDF container)
|
||||
```
|
||||
|
||||
The file then plays through the built-in decoder; no decode log appears for it. Verified both
|
||||
before and after the renderer was compiled in.
|
||||
|
||||
## Output identical to the reference renderer
|
||||
|
||||
`tests/render_harness.cpp` drives the component's own engine and writes a WAV in the same
|
||||
format the reference command-line renderer writes, so the two files can be compared byte for
|
||||
byte. 30 s of the reference file, speaker 5.1:
|
||||
|
||||
| Product | Whole-file SHA-256 |
|
||||
|---|---|
|
||||
| reference renderer (`--speaker-layout 5.1 --bed … --duration 30`) | `99a8e3edbd1c047a3c0f547eaf85e56941f882af9e65468fbde0a9f18b6c5b6e` |
|
||||
| this component's engine, x64 | `99a8e3edbd1c047a3c0f547eaf85e56941f882af9e65468fbde0a9f18b6c5b6e` |
|
||||
| this component's engine, x86 | `99a8e3edbd1c047a3c0f547eaf85e56941f882af9e65468fbde0a9f18b6c5b6e` |
|
||||
|
||||
34,578,500 bytes each. Binaural with a real SOFA file: with the same input frame count the
|
||||
whole file is identical too (`588a15ce5526977f…baa88`, 12,158,780 bytes), and over the whole
|
||||
238 s file the total sample count matches exactly (11,661,759 = 11,420,735 program +
|
||||
241,024 tail) with identical peak (1.144561172) and an identical SHA-256 over the reference
|
||||
renderer's entire payload.
|
||||
|
||||
The one structural difference is the tail: the reference renderer trims trailing samples
|
||||
below 1e-8 and this component returns the tail in full.
|
||||
|
||||
## Gain
|
||||
|
||||
Same file, speaker 5.1, rendered with and without attenuation:
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Peak | 0.183568597 → 0.092002235, i.e. exactly −6.000000 dB |
|
||||
| RMS over the whole signal | exactly −6.000000 dB |
|
||||
| Per-sample, 1,446,912 floats | largest deviation from the ideal scaling is 2.1e-06 relative; the gain is applied in double precision before the DSP, so the remaining difference is float32 rounding |
|
||||
| +6 dB | same check passes |
|
||||
| The switch | switch on and −6 dB → log `gain=-6.00 dB`, delivered peak 0.000063; switch off with −6 dB still stored → `gain=0.00 dB`, peak 0.000126; switch on and 0 dB → peak 0.000126 |
|
||||
|
||||
## Preferences page
|
||||
|
||||
`tests/prefs_layout_check.cpp` builds the page from the component's own dialog resource and
|
||||
asserts, per control, that it is enabled, lies inside the client area, and is not covered by
|
||||
another interactive control (static text and group boxes are transparent to the mouse, as
|
||||
they are for real clicks):
|
||||
|
||||
```
|
||||
dialog client=495x367 non-client=0x0
|
||||
WS_CAPTION=no WS_BORDER=no WS_CHILD=yes WS_VISIBLE=yes
|
||||
content extent=485x354 client=495x367 (everything fits)
|
||||
controls=26 problems=0
|
||||
```
|
||||
|
||||
The page has no caption of its own — the host draws the frame — and no control is ever
|
||||
disabled, which is what "the option is there but cannot be clicked" otherwise looks like.
|
||||
|
||||
Not verified here: how the page and the `%joc_*%` fields look on screen; that needs a human
|
||||
in front of the window.
|
||||
@@ -0,0 +1,157 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
foo_input_joc: foobar2000 input component (E-AC-3 JOC / Dolby Atmos).
|
||||
|
||||
Built with the official SDK's own project files, referenced below. The
|
||||
configuration name matters: the SDK ships Release (dynamic CRT) and
|
||||
Release-Static (static CRT, /MT); this component uses Release-Static so no
|
||||
runtime redistributable is required.
|
||||
|
||||
FOOBAR2000_TARGET_VERSION lives in the SDK's foobar2000-versions.h; tools/setup_sdk.ps1
|
||||
sets it to 80 (foobar2000 1.5/1.6) so the same sources load on 1.6 and on 2.x.
|
||||
-->
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Release-Static|Win32">
|
||||
<Configuration>Release-Static</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release-Static|x64">
|
||||
<Configuration>Release-Static</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{8F3A2C1E-6D4B-4F27-9E51-3C7B0A5D9E42}</ProjectGuid>
|
||||
<RootNamespace>foo_input_joc</RootNamespace>
|
||||
<ProjectName>foo_input_joc</ProjectName>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release-Static'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<UseOfMfc>false</UseOfMfc>
|
||||
<UseOfAtl>false</UseOfAtl>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutDir>$(ProjectDir)build\$(Platform)\</OutDir>
|
||||
<IntDir>$(ProjectDir)build\obj\$(Platform)\</IntDir>
|
||||
<TargetName>foo_input_joc</TargetName>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
/fp:precise is required, not cosmetic: the rendered output has to stay
|
||||
bit-identical to the reference renderer (see the project README).
|
||||
-->
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<FloatingPointModel>Precise</FloatingPointModel>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<!-- Sources are UTF-8; without this MSVC reads them in the system codepage
|
||||
and mis-parses every non-ASCII string literal. -->
|
||||
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
|
||||
<!--
|
||||
JOC_STATIC: the rendering core is linked as a static library (see the
|
||||
ProjectReference to kernel\joc_kernel.vcxproj), so kernel\include\
|
||||
joc_core.h has to expand JOC_API to nothing. Without it the header
|
||||
declares __declspec(dllimport) and the linker asks for __imp_joc_*
|
||||
symbols that no import library provides.
|
||||
-->
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDLL;WIN32_LEAN_AND_MEAN;NOMINMAX;_CRT_SECURE_NO_WARNINGS;JOC_STATIC;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ForcedIncludeFiles>$(ProjectDir)src\joc_pch.h;%(ForcedIncludeFiles)</ForcedIncludeFiles>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)src;$(ProjectDir)SDK\foobar2000;$(ProjectDir)SDK;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<DisableSpecificWarnings>4100;4127;%(DisableSpecificWarnings)</DisableSpecificWarnings>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<AdditionalDependencies>$(ProjectDir)SDK\foobar2000\shared\shared-$(Platform).lib;uxtheme.lib;comdlg32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<TargetMachine Condition="'$(Platform)'=='Win32'">MachineX86</TargetMachine>
|
||||
<TargetMachine Condition="'$(Platform)'=='x64'">MachineX64</TargetMachine>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ClCompile Include="src\eac3_scan.cpp" />
|
||||
<ClCompile Include="src\input_joc.cpp" />
|
||||
<ClCompile Include="src\joc_decode.cpp" />
|
||||
<ClCompile Include="src\log.cpp" />
|
||||
<ClCompile Include="src\main.cpp" />
|
||||
<ClCompile Include="src\prefs.cpp" />
|
||||
<ClCompile Include="src\settings.cpp" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ClInclude Include="src\eac3_scan.h" />
|
||||
<ClInclude Include="src\joc_decode.h" />
|
||||
<ClInclude Include="src\joc_pch.h" />
|
||||
<ClInclude Include="src\log.h" />
|
||||
<ClInclude Include="src\prefs.h" />
|
||||
<ClInclude Include="src\resource.h" />
|
||||
<ClInclude Include="src\settings.h" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="src\prefs.rc" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="SDK\foobar2000\SDK\foobar2000_SDK.vcxproj">
|
||||
<Project>{E8091321-D79D-4575-86EF-064EA1A4A20D}</Project>
|
||||
<Name>foobar2000_SDK</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="SDK\foobar2000\foobar2000_component_client\foobar2000_component_client.vcxproj">
|
||||
<Project>{71AD2674-065B-48F5-B8B0-E1F9D3892081}</Project>
|
||||
<Name>foobar2000_component_client</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="SDK\pfc\pfc.vcxproj">
|
||||
<Project>{EBFFFB4E-261D-44D3-B89C-957B31A0BF9C}</Project>
|
||||
<Name>pfc</Name>
|
||||
</ProjectReference>
|
||||
<!--
|
||||
The rendering core, built as a static library from the kernel copy in
|
||||
kernel\ (see kernel\joc_kernel.vcxproj). Its configurations are named
|
||||
exactly like this project's, so one reference serves both of them: MSBuild
|
||||
passes this project's Configuration/Platform straight through. Linking is
|
||||
the default (LinkLibraryDependencies), and the kernel is compiled with
|
||||
JOC_STATIC so its public headers declare nothing imported or exported.
|
||||
-->
|
||||
<ProjectReference Include="kernel\joc_kernel.vcxproj">
|
||||
<Project>{7B4E1D92-5C3A-4E0B-9F6D-2A8C41E7B530}</Project>
|
||||
<Name>joc_kernel</Name>
|
||||
</ProjectReference>
|
||||
<!--
|
||||
foobar2000_sdk_helpers and libPPUI are deliberately not referenced: both
|
||||
compile the SDK's ATL/WTL dialog framework, and WTL is an external download
|
||||
that the SDK does not ship. The decoder and the M0 probe use no dialog, so
|
||||
nothing here needs them. Whatever the preferences page uses is decided
|
||||
when that page is written.
|
||||
-->
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,239 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* EJOC_STATIC: this branch exists for building the sources directly into an application, where nothing is imported or exported. */
|
||||
#if defined(EJOC_STATIC)
|
||||
#define EJOC_API
|
||||
#define EJOC_CALL __cdecl
|
||||
#elif 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,
|
||||
EJOC_BINAURAL_BLOCK_SAMPLES = 512,
|
||||
EJOC_BINAURAL_INPUT_CHANNELS = 16,
|
||||
EJOC_BINAURAL_OUTPUT_CHANNELS = 2,
|
||||
EJOC_BINAURAL_QMF_BANDS = 64,
|
||||
EJOC_BINAURAL_HYBRID_BANDS = 77
|
||||
};
|
||||
|
||||
typedef void* ejoc_renderer_handle;
|
||||
typedef void* ejoc_speaker_renderer_handle;
|
||||
typedef void* ejoc_binaural_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.
|
||||
dq carries already dequantized matrix coefficients in double precision; the
|
||||
caller performs the JOC bitstream differential decoding for both dense and
|
||||
sparse objects, so this ABI is identical for both syntaxes.
|
||||
*/
|
||||
|
||||
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);
|
||||
|
||||
EJOC_API ejoc_binaural_renderer_handle EJOC_CALL ejoc_binaural_renderer_create(void);
|
||||
EJOC_API void EJOC_CALL ejoc_binaural_renderer_destroy(ejoc_binaural_renderer_handle handle);
|
||||
EJOC_API int EJOC_CALL ejoc_binaural_renderer_reset(ejoc_binaural_renderer_handle handle);
|
||||
EJOC_API const char* EJOC_CALL ejoc_binaural_renderer_last_error(
|
||||
ejoc_binaural_renderer_handle handle);
|
||||
EJOC_API int EJOC_CALL ejoc_binaural_renderer_configure_kernels(
|
||||
ejoc_binaural_renderer_handle handle,
|
||||
const double* qmf_analysis,
|
||||
const double* hybrid_low,
|
||||
const int16_t* hybrid_indices,
|
||||
const double* hybrid_values,
|
||||
uint32_t hybrid_count,
|
||||
const double* qmf_basis,
|
||||
const double* qmf_taps);
|
||||
EJOC_API int EJOC_CALL ejoc_binaural_renderer_configure_room(
|
||||
ejoc_binaural_renderer_handle handle,
|
||||
uint32_t bands,
|
||||
uint32_t allpass_count,
|
||||
const uint32_t* allpass_delays,
|
||||
const double* allpass_gains,
|
||||
const uint32_t* fdn_delays,
|
||||
const double* fdn_matrix,
|
||||
uint32_t output_tap_delay,
|
||||
const double* feedback_complex,
|
||||
const double* output_taps,
|
||||
const double* output_complex,
|
||||
uint32_t extra_count,
|
||||
const uint32_t* extra_delays,
|
||||
const double* extra_fields_complex,
|
||||
const double* extra_matrices);
|
||||
EJOC_API int EJOC_CALL ejoc_binaural_renderer_process(
|
||||
ejoc_binaural_renderer_handle handle,
|
||||
const double* input16_interleaved,
|
||||
const double* gains_complex,
|
||||
const double* room_sends,
|
||||
double output_gain,
|
||||
double* output_stereo_interleaved);
|
||||
|
||||
/*
|
||||
Native SOFA binaural renderer.
|
||||
|
||||
The handle owns the complete runtime: 64-QMF/77-hybrid analysis and synthesis,
|
||||
fifth-order ACN/N3D real spherical-harmonic direction-field evaluation,
|
||||
per-object whole-QMF-slot delay histories, six first-order image-source early
|
||||
reflections, the shared unitary-FDN late room, the 120-180 Hz LFE low-pass and
|
||||
the 961-sample latency compensation. The caller configures the filterbank
|
||||
tables, the compiled HRTF field and the room constants once, then per 512-sample
|
||||
block updates every source with ejoc_sofa_binaural_set_source() and calls
|
||||
ejoc_sofa_binaural_process(). Process returns the number of trimmed stereo
|
||||
samples written; the first 961 processed samples across calls are discarded.
|
||||
*/
|
||||
typedef void* ejoc_sofa_binaural_handle;
|
||||
|
||||
EJOC_API ejoc_sofa_binaural_handle EJOC_CALL ejoc_sofa_binaural_create(void);
|
||||
EJOC_API void EJOC_CALL ejoc_sofa_binaural_destroy(ejoc_sofa_binaural_handle handle);
|
||||
EJOC_API int EJOC_CALL ejoc_sofa_binaural_reset(ejoc_sofa_binaural_handle handle);
|
||||
EJOC_API const char* EJOC_CALL ejoc_sofa_binaural_last_error(
|
||||
ejoc_sofa_binaural_handle handle);
|
||||
EJOC_API int EJOC_CALL ejoc_sofa_binaural_configure_kernels(
|
||||
ejoc_sofa_binaural_handle handle,
|
||||
const double* qmf_analysis,
|
||||
const double* hybrid_low,
|
||||
const int16_t* hybrid_indices,
|
||||
const double* hybrid_values,
|
||||
uint32_t hybrid_count,
|
||||
const double* qmf_basis,
|
||||
const double* qmf_taps);
|
||||
EJOC_API int EJOC_CALL ejoc_sofa_binaural_configure_field(
|
||||
ejoc_sofa_binaural_handle handle,
|
||||
const double* coefficients,
|
||||
const double* delay_coefficients,
|
||||
const double* delay_bounds,
|
||||
const double* band_centers,
|
||||
double measurement_radius_m);
|
||||
EJOC_API int EJOC_CALL ejoc_sofa_binaural_configure_room(
|
||||
ejoc_sofa_binaural_handle handle,
|
||||
const double* room_dims,
|
||||
const double* listener_pos,
|
||||
const double* wall_gains,
|
||||
double speed_of_sound,
|
||||
const uint32_t* fdn_delays,
|
||||
const double* fdn_feedback,
|
||||
double damping,
|
||||
double fdn_output_gain,
|
||||
const uint32_t* allpass_delays,
|
||||
const double* allpass_gains,
|
||||
uint32_t enable_early_reflections,
|
||||
uint32_t enable_late_room);
|
||||
EJOC_API int EJOC_CALL ejoc_sofa_binaural_set_source(
|
||||
ejoc_sofa_binaural_handle handle,
|
||||
uint32_t source,
|
||||
const double* position_adm,
|
||||
uint32_t profile,
|
||||
double gain,
|
||||
uint32_t enabled,
|
||||
uint32_t special_lfe,
|
||||
uint32_t fade);
|
||||
EJOC_API int EJOC_CALL ejoc_sofa_binaural_process(
|
||||
ejoc_sofa_binaural_handle handle,
|
||||
const double* input16_interleaved,
|
||||
uint32_t sample_count,
|
||||
double output_gain,
|
||||
double* output_stereo_interleaved);
|
||||
EJOC_API int EJOC_CALL ejoc_sofa_binaural_finish(
|
||||
ejoc_sofa_binaural_handle handle,
|
||||
uint32_t flush_samples,
|
||||
double* output_stereo_interleaved,
|
||||
uint32_t capacity);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,490 @@
|
||||
/*
|
||||
* joc_core.h -- JustOneCacophony C++ Core, public ABI. Pure C.
|
||||
*
|
||||
* This header is the single authoritative definition of the Core's public
|
||||
* parameter/error surface. Frontends (thin Python CLI, joc_dump, foobar2000,
|
||||
* MPV, FFmpeg) only ever fill these POD structs and read these POD results;
|
||||
* no audio data, no internal DSP concept, and no Python type crosses this
|
||||
* boundary.
|
||||
*
|
||||
* Stability tiers
|
||||
* ---------------
|
||||
* [T1] host tier
|
||||
* joc_abi_version / joc_version_string / joc_build_info, joc_error,
|
||||
* joc_error_name / joc_error_stage, joc_last_error_detail.
|
||||
* Stable, versioned, safe for media hosts.
|
||||
*
|
||||
* [T2] bitstream / verification tier
|
||||
* joc_parse_eac3_frame, joc_parse_id14, joc_frame_params, joc_emdf_info.
|
||||
* These deliberately expose the dequantized JOC matrix coefficients so the
|
||||
* bitstream front-end can be validated bit-exactly and driven by tooling
|
||||
* (joc_dump) and A/B harnesses. Media hosts must NOT use this tier; they
|
||||
* use the task/stream API (added in later milestones).
|
||||
*
|
||||
* Conventions
|
||||
* -----------
|
||||
* * every struct's first two fields are struct_size / struct_version;
|
||||
* * all arrays are fixed size and POD, no pointers, no allocation;
|
||||
* * every function returns joc_error (JOC_OK == 0);
|
||||
* * joc_last_error_detail() returns a thread-local message that stays valid
|
||||
* until the next Core call on the same thread.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define JOC_ABI_VERSION 3u
|
||||
#define JOC_FRAME_PARAMS_VERSION 1u
|
||||
#define JOC_EMDF_INFO_VERSION 1u
|
||||
#define JOC_TASK_CONFIG_VERSION 3u
|
||||
#define JOC_TASK_RESULT_VERSION 2u
|
||||
#define JOC_EVENT_VERSION 1u
|
||||
|
||||
/* JOC_STATIC: this branch exists for building the sources directly into an application, where nothing is imported or exported. */
|
||||
#if defined(JOC_STATIC)
|
||||
#define JOC_API
|
||||
#define JOC_CALL __cdecl
|
||||
#elif defined(_WIN32)
|
||||
#if defined(JOC_BUILD_DLL)
|
||||
#define JOC_API __declspec(dllexport)
|
||||
#else
|
||||
#define JOC_API __declspec(dllimport)
|
||||
#endif
|
||||
#define JOC_CALL __cdecl
|
||||
#else
|
||||
#define JOC_API __attribute__((visibility("default")))
|
||||
#define JOC_CALL
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Fixed layout constants (single source of truth for every frontend) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
enum {
|
||||
JOC_FRAME_SAMPLES = 1536, /* E-AC-3 frame = 24 * 64 */
|
||||
JOC_TIMESLOTS = 24,
|
||||
JOC_SUBBANDS = 64,
|
||||
JOC_CORE_CHANNELS = 5, /* L R C Ls Rs (JOC order) */
|
||||
JOC_MAX_CORE_CHANNELS = 7, /* dmx_config_idx 1/2/4 declare 7 */
|
||||
JOC_OUTPUT_CHANNELS = 16, /* ch0 = LFE, ch1..15 = objects */
|
||||
JOC_MAX_OBJECTS = 15,
|
||||
JOC_MAX_DPOINTS = 2,
|
||||
JOC_MAX_PARAMETER_BANDS = 23,
|
||||
JOC_MAX_EMDF_PAYLOADS = 16,
|
||||
JOC_SPEAKER_BLOCK_SAMPLES = 32,
|
||||
JOC_BINAURAL_BLOCK_SAMPLES = 512,
|
||||
JOC_BINAURAL_QMF_BANDS = 64,
|
||||
JOC_BINAURAL_HYBRID_BANDS = 77,
|
||||
JOC_QMF_HOP_SAMPLES = 64,
|
||||
JOC_BINAURAL_LATENCY_SAMPLES = 961,
|
||||
JOC_LFE_DELAY_SAMPLES = 1217
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* [T1] library / error surface */
|
||||
/* ------------------------------------------------------------------ */
|
||||
typedef enum joc_error {
|
||||
JOC_OK = 0,
|
||||
JOC_ERR_INVALID_ARGUMENT,
|
||||
JOC_ERR_INVALID_CONFIG,
|
||||
JOC_ERR_OUT_OF_MEMORY,
|
||||
JOC_ERR_IO,
|
||||
JOC_ERR_UNSUPPORTED_PLATFORM,
|
||||
JOC_ERR_LIBRARY_MISSING,
|
||||
/* input / bitstream */
|
||||
JOC_ERR_INPUT_NOT_FOUND,
|
||||
JOC_ERR_INPUT_FORMAT,
|
||||
JOC_ERR_EAC3_SYNCFRAME,
|
||||
JOC_ERR_EMDF_TRANSPORT,
|
||||
JOC_ERR_EMDF_SYNTAX,
|
||||
JOC_ERR_JOC_SYNTAX,
|
||||
JOC_ERR_JOC_UNSUPPORTED_VARIANT,
|
||||
JOC_ERR_OAMD_SYNTAX,
|
||||
JOC_ERR_OAMD_UNSUPPORTED_VARIANT,
|
||||
JOC_ERR_BITSTREAM_TRUNCATED,
|
||||
JOC_ERR_BITSTREAM_PADDING,
|
||||
/* resources */
|
||||
JOC_ERR_HRTF_NOT_FOUND,
|
||||
JOC_ERR_HRTF_FORMAT,
|
||||
JOC_ERR_HRTF_VERSION,
|
||||
JOC_ERR_HRTF_HASH,
|
||||
JOC_ERR_HRTF_UNSUPPORTED_CONVENTION,
|
||||
/* rendering / output */
|
||||
JOC_ERR_LAYOUT_UNSUPPORTED,
|
||||
JOC_ERR_RENDER_FAILED,
|
||||
JOC_ERR_OUTPUT_OPEN,
|
||||
JOC_ERR_OUTPUT_WRITE,
|
||||
JOC_ERR_OUTPUT_CLIP_ABORT,
|
||||
JOC_ERR_ADM_VALIDATION,
|
||||
/* task / stream */
|
||||
JOC_ERR_CANCELLED,
|
||||
JOC_ERR_STATE,
|
||||
JOC_ERR_NOT_SUPPORTED,
|
||||
JOC_ERR_INTERNAL
|
||||
} joc_error;
|
||||
|
||||
JOC_API uint32_t JOC_CALL joc_abi_version(void);
|
||||
JOC_API const char* JOC_CALL joc_version_string(void);
|
||||
JOC_API const char* JOC_CALL joc_build_info(void);
|
||||
/* Struct sizes, so a binding can assert its layout matches the library instead of
|
||||
* assuming (mismatches are otherwise silent memory corruption). */
|
||||
JOC_API uint32_t JOC_CALL joc_event_size(void);
|
||||
JOC_API uint32_t JOC_CALL joc_task_config_size(void);
|
||||
JOC_API uint32_t JOC_CALL joc_task_result_size(void);
|
||||
JOC_API const char* JOC_CALL joc_error_name(joc_error code);
|
||||
JOC_API const char* JOC_CALL joc_error_stage(joc_error code);
|
||||
/* Thread-local structured detail for the most recent failing call. */
|
||||
JOC_API const char* JOC_CALL joc_last_error_detail(void);
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* [T2] bitstream / verification tier */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* One EMDF payload directory entry. bit_offset is the MSB-first bit
|
||||
* position of the payload's first byte inside the syncframe, exactly as the
|
||||
* EMDF container syntax defines it (payloads are not byte aligned in general). */
|
||||
typedef struct joc_emdf_payload_info {
|
||||
uint8_t id;
|
||||
uint8_t reserved[3];
|
||||
uint16_t sample_offset; /* EMDF outer smpoffst, 11 bits */
|
||||
uint16_t reserved2;
|
||||
uint32_t bit_offset;
|
||||
uint32_t size; /* payload bytes */
|
||||
} joc_emdf_payload_info;
|
||||
|
||||
typedef struct joc_emdf_info {
|
||||
uint32_t struct_size;
|
||||
uint32_t struct_version;
|
||||
uint32_t start_bit; /* container syncword bit position */
|
||||
uint32_t container_bytes; /* 4 + declared length */
|
||||
uint32_t payload_count;
|
||||
uint32_t reserved;
|
||||
joc_emdf_payload_info payloads[JOC_MAX_EMDF_PAYLOADS];
|
||||
} joc_emdf_info;
|
||||
|
||||
/* Per-object ID14/JOC descriptor plus its dequantized matrix.
|
||||
* dq[dp][ch][pb] is zero filled outside [0,n_dpoints) x [0,n_channels) x
|
||||
* [0,n_bands). Absent objects are entirely zero. */
|
||||
typedef struct joc_object_params {
|
||||
uint8_t present;
|
||||
uint8_t num_bands_idx;
|
||||
uint8_t n_bands;
|
||||
uint8_t sparse; /* 0 = dense (MTX), 1 = sparse (IDX+VEC) */
|
||||
uint8_t quant_idx; /* 0 = 96 levels, 1 = 192 levels */
|
||||
uint8_t slope_idx; /* 0 = interpolate, 1 = step at offset_ts */
|
||||
uint8_t num_dpoints_bits;
|
||||
uint8_t n_dpoints;
|
||||
uint8_t offset_ts[JOC_MAX_DPOINTS];
|
||||
uint8_t reserved[2];
|
||||
double dq[JOC_MAX_DPOINTS][JOC_MAX_CORE_CHANNELS][JOC_MAX_PARAMETER_BANDS];
|
||||
} joc_object_params;
|
||||
|
||||
typedef struct joc_frame_params {
|
||||
uint32_t struct_size;
|
||||
uint32_t struct_version;
|
||||
uint8_t dmx_config_idx;
|
||||
uint8_t num_objects_bits;
|
||||
uint8_t ext_config_idx;
|
||||
uint8_t n_objects;
|
||||
uint8_t n_channels; /* 5 or 7 */
|
||||
uint8_t clipgain_x_bits;
|
||||
uint8_t clipgain_y_bits;
|
||||
uint8_t reserved;
|
||||
uint32_t seq_count; /* 10-bit JOC sequence counter, parsed only */
|
||||
uint32_t present_mask; /* bit i == object i present */
|
||||
uint32_t data_end_bits; /* bit position just after joc_data */
|
||||
uint32_t trailing_bits; /* bits left after joc_data (padding/ext) */
|
||||
uint8_t tail_bytes[8]; /* first up to 8 trailing bytes, for A/B */
|
||||
double clipgain; /* 1 + (y/32) * 2^(x-4), bit-exact */
|
||||
joc_object_params objects[JOC_MAX_OBJECTS];
|
||||
} joc_frame_params;
|
||||
|
||||
/* Parse an EMDF ID14 (JOC) payload. */
|
||||
JOC_API joc_error JOC_CALL joc_parse_id14(const uint8_t* payload, size_t payload_size,
|
||||
joc_frame_params* out_params);
|
||||
|
||||
/* Locate the contiguous JOC EMDF container inside one E-AC-3 syncframe and
|
||||
* parse its ID14 payload. out_emdf may be NULL. */
|
||||
JOC_API joc_error JOC_CALL joc_parse_eac3_frame(const uint8_t* frame, size_t frame_size,
|
||||
joc_frame_params* out_params,
|
||||
joc_emdf_info* out_emdf);
|
||||
|
||||
/* Copy one payload's bytes out of a syncframe (MSB-first bit extraction, which
|
||||
* equals a memcpy for byte-aligned containers). out_size receives the payload
|
||||
* byte count; pass out == NULL to query only the size. */
|
||||
JOC_API joc_error JOC_CALL joc_extract_payload(const uint8_t* frame, size_t frame_size,
|
||||
const joc_emdf_payload_info* payload,
|
||||
uint8_t* out, size_t out_capacity,
|
||||
size_t* out_size);
|
||||
|
||||
/* E-AC-3 syncframe traversal: given the offset of a frame start, report its
|
||||
* byte length so a host can walk a bare E-AC-3 stream without duplicating
|
||||
* frmsiz logic. Rejects resynchronisation (no silent recovery). */
|
||||
JOC_API joc_error JOC_CALL joc_eac3_frame_bytes(const uint8_t* data, size_t size,
|
||||
size_t offset, size_t* out_frame_bytes);
|
||||
|
||||
/* Strict trailing-bit check for the JOC payload (A/B robustness corpus).
|
||||
* Returns JOC_ERR_BITSTREAM_PADDING when more than 7 bits are left over or the
|
||||
* leftover bits are not zero. */
|
||||
JOC_API joc_error JOC_CALL joc_check_id14_padding(const uint8_t* payload, size_t payload_size,
|
||||
uint32_t* out_trailing_bits);
|
||||
|
||||
/* ================================================================== */
|
||||
/* [T1] host tier: task, telemetry and control */
|
||||
/* ================================================================== */
|
||||
|
||||
/* ---- 1. events ---------------------------------------------------- */
|
||||
/*
|
||||
* Events carry state only: frame/sample counters, stage, progress, statistics,
|
||||
* warnings, errors and paths. Audio never travels through an event; a fixed
|
||||
* size POD with no pointers keeps that enforceable (see the static assertion in
|
||||
* the implementation).
|
||||
*/
|
||||
typedef enum joc_event_type {
|
||||
JOC_EV_TASK_STARTED = 0x0001,
|
||||
JOC_EV_TASK_STATE_CHANGED = 0x0002,
|
||||
JOC_EV_TASK_COMPLETED = 0x0003,
|
||||
JOC_EV_TASK_FAILED = 0x0004,
|
||||
JOC_EV_TASK_CANCELLED = 0x0005,
|
||||
JOC_EV_INPUT_OPENED = 0x0101,
|
||||
JOC_EV_METADATA_INDEXED = 0x0103,
|
||||
JOC_EV_HRTF_LOADED = 0x0110,
|
||||
JOC_EV_RENDERER_INITIALIZED = 0x0120,
|
||||
JOC_EV_PROGRESS = 0x0201,
|
||||
JOC_EV_STAGE_CHANGED = 0x0202,
|
||||
JOC_EV_JOC_FRAME_STATS = 0x0301,
|
||||
JOC_EV_OAMD_STATS = 0x0302,
|
||||
JOC_EV_OUTPUT_STATS = 0x0303,
|
||||
JOC_EV_OUTPUT_OPENED = 0x0401,
|
||||
JOC_EV_OUTPUT_FORMAT_DECIDED = 0x0402,
|
||||
JOC_EV_OUTPUT_FINALIZED = 0x0403,
|
||||
JOC_EV_LOG = 0x0501,
|
||||
JOC_EV_WARNING = 0x0502,
|
||||
JOC_EV_ERROR = 0x0503
|
||||
} joc_event_type;
|
||||
|
||||
typedef enum joc_stage {
|
||||
JOC_STAGE_IDLE = 0,
|
||||
JOC_STAGE_INPUT = 1,
|
||||
JOC_STAGE_METADATA = 2,
|
||||
JOC_STAGE_DECODE = 3,
|
||||
JOC_STAGE_JOC = 4,
|
||||
JOC_STAGE_RENDER = 5,
|
||||
JOC_STAGE_OUTPUT = 6,
|
||||
JOC_STAGE_DONE = 7
|
||||
} joc_stage;
|
||||
|
||||
enum { JOC_LOG_TRACE = 0, JOC_LOG_DEBUG = 1, JOC_LOG_INFO = 2, JOC_LOG_NOTICE = 3,
|
||||
JOC_LOG_WARNING = 4, JOC_LOG_ERROR = 5, JOC_LOG_FATAL = 6 };
|
||||
|
||||
typedef struct joc_event {
|
||||
uint32_t struct_size;
|
||||
uint32_t type;
|
||||
uint64_t sequence;
|
||||
uint64_t timestamp_us;
|
||||
uint64_t current_frame;
|
||||
uint64_t total_frames;
|
||||
uint64_t current_sample;
|
||||
uint64_t total_samples;
|
||||
uint32_t stage;
|
||||
uint32_t backend;
|
||||
double progress; /* 0..1, -1 = unknown */
|
||||
double elapsed_seconds;
|
||||
double realtime_factor;
|
||||
uint64_t output_samples;
|
||||
uint64_t output_bytes;
|
||||
double output_duration_seconds;
|
||||
joc_error error_code;
|
||||
uint32_t log_level;
|
||||
char stage_name[32];
|
||||
char message[256];
|
||||
} joc_event;
|
||||
|
||||
typedef void (JOC_CALL *joc_event_fn)(void* user, const joc_event* event);
|
||||
|
||||
typedef struct joc_event_sink {
|
||||
uint32_t struct_size;
|
||||
joc_event_fn callback;
|
||||
void* user;
|
||||
uint32_t min_type; /* 0 = no filter */
|
||||
uint32_t max_type; /* 0 = no filter */
|
||||
} joc_event_sink;
|
||||
|
||||
/* ---- 2. cancellation ---------------------------------------------- */
|
||||
typedef struct joc_cancel_token joc_cancel_token;
|
||||
|
||||
JOC_API joc_cancel_token* JOC_CALL joc_cancel_token_create(void);
|
||||
JOC_API void JOC_CALL joc_cancel_token_request(joc_cancel_token* token);
|
||||
JOC_API int32_t JOC_CALL joc_cancel_token_is_requested(const joc_cancel_token* token);
|
||||
JOC_API void JOC_CALL joc_cancel_token_destroy(joc_cancel_token* token);
|
||||
|
||||
/* ---- 3. task configuration and results ---------------------------- */
|
||||
typedef enum joc_operation {
|
||||
JOC_OP_ADM_BWF = 0,
|
||||
JOC_OP_SPEAKER = 1,
|
||||
JOC_OP_BINAURAL = 2
|
||||
} joc_operation;
|
||||
|
||||
typedef enum joc_output_format {
|
||||
JOC_FORMAT_FLOAT32 = 0,
|
||||
JOC_FORMAT_PCM24 = 1
|
||||
} joc_output_format;
|
||||
|
||||
typedef enum joc_binaural_mode {
|
||||
JOC_BINAURAL_OFF = 0,
|
||||
JOC_BINAURAL_NEAR = 1,
|
||||
JOC_BINAURAL_FAR = 2,
|
||||
JOC_BINAURAL_MID = 3
|
||||
} joc_binaural_mode;
|
||||
|
||||
typedef enum joc_trajectory_mode {
|
||||
JOC_TRAJECTORY_COMPACT = 0,
|
||||
JOC_TRAJECTORY_DENSE64 = 1
|
||||
} joc_trajectory_mode;
|
||||
|
||||
/* What to do when an int24 WAV would clip (peak outside [-1, 1]). Mirrors the
|
||||
* reference CLI's --clip-action; ADM BWF output is always int24 and does not
|
||||
* consult this because no alternative format exists there. */
|
||||
typedef enum joc_clip_action {
|
||||
JOC_CLIP_ASK = 0, /* prompt on stdin; an error when stdin is not a terminal */
|
||||
JOC_CLIP_CONTINUE = 1, /* write int24, truncating out-of-range values */
|
||||
JOC_CLIP_FLOAT32 = 2, /* switch the output to float32 */
|
||||
JOC_CLIP_ABORT = 3 /* fail the task */
|
||||
} joc_clip_action;
|
||||
|
||||
/* Compiled-HRTF cache policy for a SOFA input; the .jochrtf itself is an
|
||||
* internal artifact of the compile step. */
|
||||
typedef enum joc_hrtf_cache_policy {
|
||||
JOC_HRTF_CACHE_NONE = 0, /* compile and discard */
|
||||
JOC_HRTF_CACHE_MEMORY = 1, /* compile and keep in this process (default) */
|
||||
JOC_HRTF_CACHE_DISK = 2 /* compile, reuse and write <cache_dir>/<name>.jochrtf */
|
||||
} joc_hrtf_cache_policy;
|
||||
|
||||
enum { JOC_TASK_F_SKIP_SHA256 = 1u, JOC_TASK_F_KEEP_INTERMEDIATE = 2u,
|
||||
JOC_TASK_F_QUIET = 4u, JOC_TASK_F_METADATA_ONLY = 8u };
|
||||
|
||||
typedef struct joc_task_config {
|
||||
uint32_t struct_size;
|
||||
uint32_t struct_version;
|
||||
|
||||
/* input */
|
||||
const char* input_path; /* .eac3/.ec3/.m4a/... */
|
||||
const char* ffmpeg_path; /* NULL = "ffmpeg" from PATH */
|
||||
const char* bed_path; /* NULL = decode the core PCM with ffmpeg */
|
||||
const char* work_dir; /* NULL = a temporary directory */
|
||||
double eac3_drc_scale; /* 0 = DRC off (reference default) */
|
||||
int32_t eac3_target_level; /* -31..0, 0 = not applied */
|
||||
|
||||
/* output */
|
||||
uint32_t operation;
|
||||
const char* output_path;
|
||||
uint32_t output_format; /* requested format for speaker/binaural */
|
||||
uint32_t flags;
|
||||
uint32_t clip_action; /* joc_clip_action; JOC_CLIP_ASK by default */
|
||||
|
||||
/* rendering */
|
||||
const char* speaker_layout_name;
|
||||
uint32_t speaker_metadata_offset; /* default 1473 */
|
||||
uint32_t binaural_mode;
|
||||
const char* hrtf_path; /* .jochrtf */
|
||||
const char* kernels_path; /* rosella_kernels.npz */
|
||||
double binaural_tail_seconds; /* default 5.0 */
|
||||
double binaural_tail_threshold; /* binaural only; 0 disables trimming */
|
||||
uint32_t binaural_chunk_frames; /* accepted for CLI parity; no effect */
|
||||
|
||||
/* ADM */
|
||||
uint32_t adm_binaural_mode; /* DBMD segment 10 encoding */
|
||||
uint32_t trajectory_mode;
|
||||
|
||||
/* generic */
|
||||
uint32_t object_delay_samples; /* default 1473 */
|
||||
double gain_db; /* default 0 */
|
||||
uint64_t duration_frames; /* 0 = the whole stream */
|
||||
uint32_t progress_interval_frames; /* default 1000 */
|
||||
uint32_t native_threads; /* 0 = automatic */
|
||||
|
||||
/* diagnostics */
|
||||
uint32_t print_metadata; /* 0 none, 1 summary, 2 per frame */
|
||||
const char* metadata_json_path; /* NULL = no JSON summary */
|
||||
|
||||
joc_cancel_token* cancel; /* optional */
|
||||
|
||||
/* Binaural HRTF input (config version 2): when hrtf_sofa_path is set the
|
||||
* library compiles it with hrtf_cache_policy / hrtf_cache_dir / hrtf_radius_m
|
||||
* and hrtf_path is unused. hrtf_path stays the advanced override that reads
|
||||
* a .jochrtf directly. */
|
||||
const char* hrtf_sofa_path; /* SOFA SimpleFreeFieldHRIR input */
|
||||
const char* personalized_headphone_path; /* Rosella .personalized_headphone input */
|
||||
const char* hrtf_cache_dir; /* disk policy directory */
|
||||
uint32_t hrtf_cache_policy; /* joc_hrtf_cache_policy */
|
||||
uint32_t reserved0;
|
||||
double hrtf_radius_m; /* SOFA measurement-radius shell */
|
||||
} joc_task_config;
|
||||
|
||||
typedef enum joc_task_status {
|
||||
JOC_TASK_OK = 0,
|
||||
JOC_TASK_FAILED = 1,
|
||||
JOC_TASK_CANCELLED = 2
|
||||
} joc_task_status;
|
||||
|
||||
typedef struct joc_task_result {
|
||||
uint32_t struct_size;
|
||||
uint32_t struct_version;
|
||||
uint32_t status;
|
||||
uint32_t error_code;
|
||||
char error_message[512];
|
||||
char error_stage[32];
|
||||
uint64_t input_frames;
|
||||
uint64_t output_samples;
|
||||
double duration_sec;
|
||||
uint32_t output_format_actual;
|
||||
double output_peak;
|
||||
uint64_t output_over_unity_values;
|
||||
uint64_t output_file_bytes;
|
||||
char output_sha256[65]; /* empty when skipped */
|
||||
uint64_t oamd_payloads;
|
||||
uint64_t oamd_transitions;
|
||||
double t_decode_bed;
|
||||
double t_render;
|
||||
double t_write;
|
||||
double t_total;
|
||||
double t_render_dsp; /* speaker/binaural DSP calls only; 0 for ADM */
|
||||
double t_write_file; /* disk writes including the finalize; >= t_write */
|
||||
} joc_task_result;
|
||||
|
||||
typedef struct joc_validation_issue {
|
||||
joc_error code;
|
||||
uint32_t severity; /* 0 = info, 1 = warning, 2 = error */
|
||||
char field[48];
|
||||
char message[256];
|
||||
} joc_validation_issue;
|
||||
|
||||
/* ---- 4. entry points ---------------------------------------------- */
|
||||
/* Fills `issues` (up to `capacity`) and reports how many were produced.
|
||||
* Returns JOC_OK when no *error*-severity issue was found. */
|
||||
JOC_API joc_error JOC_CALL joc_task_validate(const joc_task_config* config,
|
||||
joc_validation_issue* issues, uint32_t capacity,
|
||||
uint32_t* count);
|
||||
|
||||
/* Runs the whole task on the calling thread. `sink` may be NULL; `out` may be
|
||||
* NULL. On cancellation the partial output is removed and JOC_ERR_CANCELLED is
|
||||
* returned with out->status = JOC_TASK_CANCELLED. */
|
||||
JOC_API joc_error JOC_CALL joc_task_execute(const joc_task_config* config,
|
||||
const joc_event_sink* sink, joc_task_result* out);
|
||||
|
||||
/* Serialises the stable subset of the result as JSON (no environment fields -
|
||||
* those belong to the frontend). `needed` receives the required size including
|
||||
* the terminator; a NULL buffer queries only the size. */
|
||||
JOC_API joc_error JOC_CALL joc_task_result_to_json(const joc_task_result* result, char* buffer,
|
||||
size_t capacity, size_t* needed);
|
||||
|
||||
/* The streaming (push/pull) tier lives in "joc_stream.h" so an embedder can
|
||||
* include the narrow surface without the bitstream/verification tier. */
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* joc_stream.h -- streaming (push/pull) interface of the JustOneCacophony core.
|
||||
*
|
||||
* This is the narrow, embedder-facing surface: a player or decoder component
|
||||
* includes only this header. It is the same shared library as joc_core.h, split
|
||||
* so an integrator never has to see the bitstream/verification tier.
|
||||
*
|
||||
* A stream is a stateful instance for hosts that cannot wait for a whole file:
|
||||
* the caller feeds E-AC-3 bytes and the core PCM of the same frames (or already
|
||||
* rebuilt objects16) and pulls rendered PCM as soon as it is available. It
|
||||
* mirrors the two library shapes a decoder library normally offers: this
|
||||
* push/pull form for host-owned I/O, and joc_task_execute() in joc_core.h for
|
||||
* library-owned file I/O.
|
||||
*
|
||||
* Contract:
|
||||
* - all state is instance-private, so several streams coexist;
|
||||
* - a stream is NOT thread safe: push and pull must come from one thread;
|
||||
* - rendering is stateful (matrix interpolation, gain ramps, room tail), so a
|
||||
* new position on the timeline requires decoding to continue from the start
|
||||
* of the stream; there is no seek in this version;
|
||||
* - the kernel latency is 961 samples for speaker/binaural output: the first
|
||||
* pull after two 512-sample blocks, and flush() drains the binaural tail.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "joc_core.h"
|
||||
|
||||
/* JOC_STATIC (building the sources directly into an application): joc_core.h above already installs the empty JOC_API. */
|
||||
#if defined(JOC_STATIC) && !defined(JOC_API)
|
||||
#define JOC_API
|
||||
#define JOC_CALL __cdecl
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct joc_stream joc_stream;
|
||||
|
||||
typedef enum joc_stream_input {
|
||||
JOC_STREAM_IN_EAC3 = 0, /* bare E-AC-3 syncframes (the metadata stream) */
|
||||
JOC_STREAM_IN_PCM_OBJECTS16 = 1, /* 16-channel objects16, decoded by the host */
|
||||
JOC_STREAM_IN_CORE_PCM = 3 /* the 5.1 core PCM of the pushed E-AC-3 frames */
|
||||
} joc_stream_input;
|
||||
|
||||
typedef enum joc_stream_output {
|
||||
JOC_STREAM_OUT_PCM_OBJECTS16 = 0, /* planar [16][samples] float32 */
|
||||
JOC_STREAM_OUT_SPEAKER = 1, /* interleaved [samples][channels] f32 */
|
||||
JOC_STREAM_OUT_BINAURAL = 2 /* interleaved [samples][2] f32 */
|
||||
} joc_stream_output;
|
||||
|
||||
typedef struct joc_stream_config {
|
||||
uint32_t struct_size;
|
||||
uint32_t struct_version;
|
||||
uint32_t input;
|
||||
uint32_t output;
|
||||
const char* speaker_layout_name;
|
||||
uint32_t speaker_metadata_offset; /* default 1473 */
|
||||
uint32_t binaural_mode; /* near|mid|far; 0 means mid */
|
||||
const char* hrtf_path; /* binaural only */
|
||||
const char* kernels_path; /* binaural only */
|
||||
double binaural_tail_seconds; /* default 5.0 */
|
||||
uint32_t object_delay_samples; /* default 1473 */
|
||||
double gain_db; /* default 0 */
|
||||
uint32_t native_threads;
|
||||
uint32_t reserved;
|
||||
/* Binaural HRTF input, the same three shapes joc_task_config accepts: when
|
||||
* hrtf_sofa_path is set the library compiles it with hrtf_cache_policy /
|
||||
* hrtf_cache_dir / hrtf_radius_m and hrtf_path is unused; when
|
||||
* personalized_headphone_path is set the Rosella runtime renders instead.
|
||||
* hrtf_path stays the fallback/advanced input that reads a .jochrtf directly. */
|
||||
const char* hrtf_sofa_path; /* SOFA SimpleFreeFieldHRIR input */
|
||||
const char* personalized_headphone_path; /* Rosella .personalized_headphone input */
|
||||
const char* hrtf_cache_dir; /* disk cache directory for SOFA compilation */
|
||||
uint32_t hrtf_cache_policy; /* joc_hrtf_cache_policy: 0 none, 1 memory, 2 disk */
|
||||
double hrtf_radius_m; /* SOFA measurement-radius shell, default 1.0 */
|
||||
} joc_stream_config;
|
||||
|
||||
typedef struct joc_stream_buffer {
|
||||
uint32_t struct_size;
|
||||
uint32_t struct_version;
|
||||
uint32_t kind; /* which joc_stream_input/output this buffer carries */
|
||||
uint32_t channels;
|
||||
uint32_t sample_rate;
|
||||
uint32_t sample_count; /* in: capacity / out: produced (per channel) */
|
||||
uint32_t byte_count; /* in: capacity / out: consumed or produced bytes */
|
||||
uint32_t reserved;
|
||||
const uint8_t* bytes; /* EAC3 input */
|
||||
const float* pcm; /* PCM input */
|
||||
uint8_t* out_bytes; /* reserved for encoded outputs */
|
||||
float* out_pcm; /* PCM output */
|
||||
} joc_stream_buffer;
|
||||
|
||||
typedef struct joc_stream_status_info {
|
||||
uint32_t struct_size;
|
||||
uint32_t struct_version;
|
||||
uint64_t frames_in;
|
||||
uint64_t frames_out;
|
||||
uint64_t samples_in;
|
||||
uint64_t samples_out;
|
||||
uint64_t bytes_in;
|
||||
uint64_t buffered_samples; /* rendered but not yet pulled, per channel */
|
||||
uint64_t oamd_payloads;
|
||||
uint64_t oamd_transitions;
|
||||
uint32_t output_channels;
|
||||
uint32_t ended; /* 1 after flush() */
|
||||
} joc_stream_status_info;
|
||||
|
||||
JOC_API joc_error JOC_CALL joc_stream_create(const joc_stream_config* config, joc_stream** out);
|
||||
|
||||
/* Feeds one buffer. Kind selects the path: EAC3 bytes, the core PCM of those
|
||||
* frames, or objects16. Consumed counts are reported so a caller can resume
|
||||
* from a partial push. */
|
||||
JOC_API joc_error JOC_CALL joc_stream_push(joc_stream* stream, const joc_stream_buffer* input,
|
||||
uint32_t* consumed_samples, uint32_t* consumed_bytes);
|
||||
|
||||
/* Copies as many rendered samples as fit into `output` (interleaved, or planar
|
||||
* for PCM_OBJECTS16) and reports how many were produced; 0 means "push more". */
|
||||
JOC_API joc_error JOC_CALL joc_stream_pull(joc_stream* stream, joc_stream_buffer* output,
|
||||
uint32_t* produced_samples);
|
||||
|
||||
/* Marks the end of input and drains whatever the renderer still holds (the
|
||||
* binaural room tail); pull the remaining samples afterwards. */
|
||||
JOC_API joc_error JOC_CALL joc_stream_flush(joc_stream* stream);
|
||||
|
||||
/* Returns the instance to its initial state (kernel, ramps, timeline, room,
|
||||
* parser, counters) so the same stream can be reused for another pass. */
|
||||
JOC_API joc_error JOC_CALL joc_stream_reset(joc_stream* stream);
|
||||
|
||||
JOC_API joc_error JOC_CALL joc_stream_status(const joc_stream* stream,
|
||||
joc_stream_status_info* out);
|
||||
JOC_API joc_error JOC_CALL joc_stream_destroy(joc_stream* stream);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
@@ -0,0 +1,257 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
joc_kernel: the JustOneCacophony rendering core, built as a static library
|
||||
(/MT) so the foobar2000 component can link the engine directly instead of
|
||||
shipping and loading joc_core.dll.
|
||||
|
||||
The sources under kernel\src and kernel\include are a verbatim copy of the
|
||||
upstream library (see JustOneCacophonyCXX\CMakeLists.txt); the only edits are
|
||||
the JOC_STATIC branch in kernel\include\joc_core.h / joc_stream.h and the
|
||||
EJOC_STATIC branch in kernel\include\eac3joc_core.h, which turn JOC_API and
|
||||
EJOC_API into empty macros for a static build.
|
||||
|
||||
Conventions deliberately mirror the component project (foo_input_joc.vcxproj)
|
||||
and the kernel's own CMake build:
|
||||
* configuration Release-Static, static CRT (/MT, MultiThreaded), no
|
||||
redistributable;
|
||||
* /fp:precise is mandatory, not cosmetic: the rendered output has to stay
|
||||
bit-identical to the reference renderer, so no /fp:fast, no fast-math;
|
||||
* /utf-8 (the sources are UTF-8) and /Zc:__cplusplus (joc_api.cpp reports
|
||||
__cplusplus in its build-info string; without it MSVC answers 199711L);
|
||||
* C++20, /permissive-, /W4, /O2 /Oi: the same flags CMake gives the MSVC
|
||||
targets (JOC_MSVC_FLAGS in CMakeLists.txt), minus the /arch flags, which
|
||||
belong to the individual SIMD units only.
|
||||
|
||||
ForcedIncludeFiles is NOT set: the component's src\joc_pch.h exists to pull
|
||||
the standard library in ahead of the foobar2000 SDK headers, and the kernel
|
||||
copy is a self-contained upstream tree that its own build force-includes
|
||||
nothing into. Keeping the kernel's compile environment equal to the one the
|
||||
library is validated with is what the bit-exactness contract depends on.
|
||||
-->
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Release-Static|Win32">
|
||||
<Configuration>Release-Static</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release-Static|x64">
|
||||
<Configuration>Release-Static</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{7B4E1D92-5C3A-4E0B-9F6D-2A8C41E7B530}</ProjectGuid>
|
||||
<RootNamespace>joc_kernel</RootNamespace>
|
||||
<ProjectName>joc_kernel</ProjectName>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release-Static'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<UseOfMfc>false</UseOfMfc>
|
||||
<UseOfAtl>false</UseOfAtl>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
|
||||
<!--
|
||||
The .lib lands next to the component's output: build\kernel-<Platform>\.
|
||||
A separate IntDir per platform keeps the two architectures' objects apart.
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<OutDir>$(ProjectDir)..\build\kernel-$(Platform)\</OutDir>
|
||||
<IntDir>$(ProjectDir)..\build\obj\joc_kernel\$(Platform)\</IntDir>
|
||||
<TargetName>joc_kernel</TargetName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<RuntimeTypeInfo>false</RuntimeTypeInfo>
|
||||
<ExceptionHandling>Sync</ExceptionHandling>
|
||||
<FloatingPointModel>Precise</FloatingPointModel>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<AdditionalOptions>/utf-8 /Zc:__cplusplus %(AdditionalOptions)</AdditionalOptions>
|
||||
<!--
|
||||
JOC_STATIC: the public headers declare nothing imported or exported
|
||||
(see kernel\include\joc_core.h), because these sources are archived
|
||||
into a .lib instead of the upstream joc_core.dll.
|
||||
EJOC_STATIC does the same for the inner engine entry points declared in
|
||||
kernel\include\eac3joc_core.h: without it the units that call them
|
||||
(task.cpp, stream.cpp, objects16.cpp, binaural_runtime.cpp, speaker_*)
|
||||
see __declspec(dllimport), emit __imp_ejoc_* references and the archive
|
||||
can never be linked, because a static library has no import library to
|
||||
resolve them from. It covers the defining units too, including
|
||||
src\joc_core\eac3joc_core.cpp, so the whole copy agrees on plain
|
||||
symbols. No hardware-related or numerical behaviour is involved.
|
||||
NDEBUG and _CRT_SECURE_NO_WARNINGS are the two defines the kernel's own
|
||||
MSVC build passes (JOC_MSVC_DEFINES in CMakeLists.txt); the writers use
|
||||
std::fopen on purpose, for seekable byte-exact output.
|
||||
-->
|
||||
<PreprocessorDefinitions>NDEBUG;_CRT_SECURE_NO_WARNINGS;JOC_STATIC;EJOC_STATIC;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)include;$(ProjectDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<!--
|
||||
SIMD units, mirroring CMakeLists.txt (the JOC_SIMD_* block) exactly.
|
||||
|
||||
The ISA in the file name, one translation unit per ISA, each compiled with
|
||||
its own /arch because MSVC has no function-level ISA attribute; dispatch.cpp
|
||||
(a baseline unit) picks one at run time from CPUID/XGETBV. The kernel
|
||||
enables SIMD only for x86_64 and aarch64, so:
|
||||
|
||||
* Win32 (x86) gets NO SIMD unit and NO SIMD define: the scalar reference
|
||||
and the probe/dispatch baseline are all it builds, exactly as
|
||||
CMAKE_SIZEOF_VOID_P EQUAL 8 gates them out upstream;
|
||||
* x64 gets the AVX2 and AVX-512 units, JOC_SIMD_HAVE_SSE2 / _AVX2 /
|
||||
_AVX512, and a per-file /arch for those two files only;
|
||||
* src\simd\kernels_intrin_neon.cpp is excluded everywhere here: the CMake
|
||||
build lists it only for aarch64 (CMakeLists.txt lines 177-180), which no
|
||||
configuration of this project targets.
|
||||
-->
|
||||
<ItemDefinitionGroup Condition="'$(Platform)'=='x64'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>JOC_SIMD_HAVE_SSE2=1;JOC_SIMD_HAVE_AVX2=1;JOC_SIMD_HAVE_AVX512=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Verbatim copies of the upstream native library (JOC_REUSED_SOURCES). -->
|
||||
<ClCompile Include="src\joc_core\eac3joc_core.cpp" />
|
||||
<ClCompile Include="src\speaker\speaker_renderer.cpp" />
|
||||
<ClCompile Include="src\binaural\binaural_renderer.cpp" />
|
||||
<!-- Engine internals (JOC_INTERNAL_SOURCES). -->
|
||||
<ClCompile Include="src\adm\adm_metadata.cpp" />
|
||||
<ClCompile Include="src\adm\adm_tracks.cpp" />
|
||||
<ClCompile Include="src\binaural\binaural_runtime.cpp" />
|
||||
<ClCompile Include="src\binaural\sofa_binaural_renderer.cpp" />
|
||||
<ClCompile Include="src\eac3_transport\eac3_reader.cpp" />
|
||||
<ClCompile Include="src\emdf\emdf_parser.cpp" />
|
||||
<ClCompile Include="src\foundation\bit_reader.cpp" />
|
||||
<ClCompile Include="src\foundation\fft.cpp" />
|
||||
<ClCompile Include="src\foundation\fs_utf8.cpp" />
|
||||
<ClCompile Include="src\foundation\mini_json.cpp" />
|
||||
<ClCompile Include="src\foundation\sha256.cpp" />
|
||||
<ClCompile Include="src\hrtf\jochrtf.cpp" />
|
||||
<ClCompile Include="src\hrtf\public_filterbank.cpp" />
|
||||
<ClCompile Include="src\hrtf\rosella_model.cpp" />
|
||||
<ClCompile Include="src\hrtf\rosella_renderer.cpp" />
|
||||
<ClCompile Include="src\hrtf\kernel_tables.cpp" />
|
||||
<ClCompile Include="src\hrtf\sofa.cpp" />
|
||||
<ClCompile Include="src\hrtf\sofa_cache.cpp" />
|
||||
<ClCompile Include="src\hrtf\sofa_field.cpp" />
|
||||
<ClCompile Include="src\io\adm_writer.cpp" />
|
||||
<ClCompile Include="src\io\hdf5.cpp" />
|
||||
<ClCompile Include="src\io\inflate.cpp" />
|
||||
<ClCompile Include="src\io\npy.cpp" />
|
||||
<ClCompile Include="src\io\npy_writer.cpp" />
|
||||
<ClCompile Include="src\io\process.cpp" />
|
||||
<ClCompile Include="src\io\wav_writer.cpp" />
|
||||
<ClCompile Include="src\io\zip_reader.cpp" />
|
||||
<ClCompile Include="src\joc_bitstream\joc_parser.cpp" />
|
||||
<ClCompile Include="src\joc_core\objects16.cpp" />
|
||||
<ClCompile Include="src\oamd\oamd_parser.cpp" />
|
||||
<ClCompile Include="src\speaker\speaker_layout_lookup.cpp" />
|
||||
<ClCompile Include="src\speaker\speaker_step.cpp" />
|
||||
<ClCompile Include="src\stream\stream.cpp" />
|
||||
<ClCompile Include="src\task\task.cpp" />
|
||||
<ClCompile Include="src\telemetry\event_bus.cpp" />
|
||||
<ClCompile Include="src\timeline\position_timeline.cpp" />
|
||||
<!-- Public C API (the dll only upstream; JOC_STATIC makes it a plain lib). -->
|
||||
<ClCompile Include="src\api\joc_api.cpp" />
|
||||
<ClCompile Include="src\api\joc_stream_api.cpp" />
|
||||
<ClCompile Include="src\api\joc_task_api.cpp" />
|
||||
<!-- Dispatcher baseline: compiled for the target's own ISA, no /arch. -->
|
||||
<ClCompile Include="src\simd\cpu_probe.cpp" />
|
||||
<ClCompile Include="src\simd\dispatch.cpp" />
|
||||
<ClCompile Include="src\simd\kernels_scalar.cpp" />
|
||||
<!--
|
||||
The CLI frontend. Upstream builds it into joc_cli.exe out of the same
|
||||
sources; it is part of the copy, it needs nothing but the library API, and
|
||||
a static library keeps it inert (its main() is never referenced, so the
|
||||
linker never pulls the object in) while the copy stays complete.
|
||||
-->
|
||||
<ClCompile Include="src\cli\joc_cli.cpp" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(Platform)'=='x64'">
|
||||
<ClCompile Include="src\simd\kernels_intrin_avx2.cpp">
|
||||
<AdditionalOptions>/arch:AVX2 %(AdditionalOptions)</AdditionalOptions>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\simd\kernels_intrin_avx512.cpp">
|
||||
<AdditionalOptions>/arch:AVX512 %(AdditionalOptions)</AdditionalOptions>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ClInclude Include="include\eac3joc_core.h" />
|
||||
<ClInclude Include="include\joc_core.h" />
|
||||
<ClInclude Include="include\joc_stream.h" />
|
||||
<ClInclude Include="src\adm\adm_metadata.h" />
|
||||
<ClInclude Include="src\adm\adm_tracks.h" />
|
||||
<ClInclude Include="src\binaural\binaural_runtime.h" />
|
||||
<ClInclude Include="src\eac3_transport\eac3_reader.h" />
|
||||
<ClInclude Include="src\emdf\emdf_parser.h" />
|
||||
<ClInclude Include="src\foundation\bit_reader.h" />
|
||||
<ClInclude Include="src\foundation\fft.h" />
|
||||
<ClInclude Include="src\foundation\fs_utf8.h" />
|
||||
<ClInclude Include="src\foundation\geometry.h" />
|
||||
<ClInclude Include="src\foundation\mini_json.h" />
|
||||
<ClInclude Include="src\foundation\py_num.h" />
|
||||
<ClInclude Include="src\foundation\sha256.h" />
|
||||
<ClInclude Include="src\foundation\status.h" />
|
||||
<ClInclude Include="src\hrtf\jochrtf.h" />
|
||||
<ClInclude Include="src\hrtf\public_filterbank.h" />
|
||||
<ClInclude Include="src\hrtf\rosella_model.h" />
|
||||
<ClInclude Include="src\hrtf\rosella_renderer.h" />
|
||||
<ClInclude Include="src\hrtf\sofa.h" />
|
||||
<ClInclude Include="src\hrtf\sofa_cache.h" />
|
||||
<ClInclude Include="src\hrtf\sofa_field.h" />
|
||||
<ClInclude Include="src\io\adm_writer.h" />
|
||||
<ClInclude Include="src\io\hdf5.h" />
|
||||
<ClInclude Include="src\io\inflate.h" />
|
||||
<ClInclude Include="src\io\npy.h" />
|
||||
<ClInclude Include="src\io\npy_writer.h" />
|
||||
<ClInclude Include="src\io\process.h" />
|
||||
<ClInclude Include="src\io\wav_writer.h" />
|
||||
<ClInclude Include="src\io\zip_reader.h" />
|
||||
<ClInclude Include="src\joc_bitstream\joc_huffman_tables.h" />
|
||||
<ClInclude Include="src\joc_bitstream\joc_parser.h" />
|
||||
<ClInclude Include="src\joc_core\objects16.h" />
|
||||
<ClInclude Include="src\joc_core\qmf_tables.h" />
|
||||
<ClInclude Include="src\oamd\oamd_parser.h" />
|
||||
<ClInclude Include="src\simd\cpu_probe.h" />
|
||||
<ClInclude Include="src\simd\simd.h" />
|
||||
<!-- src\simd\kernels_intrin_neon.cpp is excluded on every platform; see above. -->
|
||||
<ClInclude Include="src\speaker\speaker_layout_lookup.h" />
|
||||
<ClInclude Include="src\speaker\speaker_layouts.h" />
|
||||
<ClInclude Include="src\speaker\speaker_step.h" />
|
||||
<ClInclude Include="src\stream\stream.h" />
|
||||
<ClInclude Include="src\task\task.h" />
|
||||
<ClInclude Include="src\telemetry\event_bus.h" />
|
||||
<ClInclude Include="src\timeline\position_timeline.h" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,333 @@
|
||||
#include "adm/adm_metadata.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
#include "foundation/py_num.h"
|
||||
|
||||
namespace joc::adm {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kBedNames[10] = {
|
||||
"RoomCentricLeft", "RoomCentricRight", "RoomCentricCenter", "RoomCentricLFE",
|
||||
"RoomCentricLeftSideSurround", "RoomCentricRightSideSurround",
|
||||
"RoomCentricLeftRearSurround", "RoomCentricRightRearSurround",
|
||||
"RoomCentricLeftTopSurround", "RoomCentricRightTopSurround"};
|
||||
constexpr const char* kBedLabels[10] = {"RC_L", "RC_R", "RC_C", "RC_LFE", "RC_Lss",
|
||||
"RC_Rss", "RC_Lrs", "RC_Rrs", "RC_Lts", "RC_Rts"};
|
||||
constexpr double kBedPos[10][3] = {{-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}};
|
||||
|
||||
std::string hex4(std::uint32_t value) {
|
||||
char buffer[16];
|
||||
std::snprintf(buffer, sizeof(buffer), "%04x", value);
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
std::string hex8(std::uint32_t value) {
|
||||
char buffer[16];
|
||||
std::snprintf(buffer, sizeof(buffer), "%08x", value);
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
void put_u16(std::string* out, std::uint16_t value) {
|
||||
char buffer[2];
|
||||
std::memcpy(buffer, &value, 2);
|
||||
out->append(buffer, 2);
|
||||
}
|
||||
|
||||
void put_u32(std::string* out, std::uint32_t value) {
|
||||
char buffer[4];
|
||||
std::memcpy(buffer, &value, 4);
|
||||
out->append(buffer, 4);
|
||||
}
|
||||
|
||||
std::uint8_t checksum(const std::string& segment) {
|
||||
int sum = static_cast<int>(segment.size());
|
||||
for (const char raw : segment) {
|
||||
sum += static_cast<unsigned char>(raw);
|
||||
}
|
||||
return static_cast<std::uint8_t>((~sum + 1) & 0xFF);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string ts(double seconds) {
|
||||
long long whole = static_cast<long long>(seconds);
|
||||
long long fraction = pynum::py_round((seconds - static_cast<double>(whole)) * 100000.0);
|
||||
if (fraction >= 100000) {
|
||||
whole += 1;
|
||||
fraction = 0;
|
||||
}
|
||||
char buffer[32];
|
||||
std::snprintf(buffer, sizeof(buffer), "%02lld:%02lld:%02lld.%05lld", whole / 3600,
|
||||
(whole % 3600) / 60, whole % 60, fraction);
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
bool binaural_mode_from_name(const char* name, BinauralMode* out) {
|
||||
if (name == nullptr || out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (std::strcmp(name, "off") == 0) { *out = BinauralMode::Off; return true; }
|
||||
if (std::strcmp(name, "near") == 0) { *out = BinauralMode::Near; return true; }
|
||||
if (std::strcmp(name, "far") == 0) { *out = BinauralMode::Far; return true; }
|
||||
if (std::strcmp(name, "mid") == 0) { *out = BinauralMode::Mid; return true; }
|
||||
if (std::strcmp(name, "unspecified") == 0) { *out = BinauralMode::Unspecified; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string build_chna() {
|
||||
std::string out;
|
||||
put_u16(&out, static_cast<std::uint16_t>(kTrackCount));
|
||||
put_u16(&out, static_cast<std::uint16_t>(kTrackCount));
|
||||
for (std::uint32_t i = 0; i < 10; ++i) {
|
||||
put_u16(&out, static_cast<std::uint16_t>(i + 1));
|
||||
out += "ATU_" + hex8(i + 1);
|
||||
out += "AT_0001" + hex4(0x1001 + i) + "_01";
|
||||
out += "AP_00011001";
|
||||
out.push_back('\0');
|
||||
}
|
||||
for (std::uint32_t i = 0; i < kObjectCount; ++i) {
|
||||
put_u16(&out, static_cast<std::uint16_t>(i + 11));
|
||||
out += "ATU_" + hex8(i + 11);
|
||||
out += "AT_0003" + hex4(0x1001 + i) + "_01";
|
||||
out += "AP_0003" + hex4(0x1001 + i);
|
||||
out.push_back('\0');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Status build_dbmd(std::uint32_t object_count, BinauralMode mode, std::string* out) {
|
||||
if (out == nullptr) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOutput, "null output");
|
||||
}
|
||||
const std::uint32_t mode_value = static_cast<std::uint32_t>(mode);
|
||||
if (mode_value > 4u) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOutput,
|
||||
"invalid JOC binaural render mode");
|
||||
}
|
||||
out->clear();
|
||||
put_u32(out, 0x01000006u);
|
||||
|
||||
std::string segment7(96, '\0');
|
||||
segment7[1] = static_cast<char>(0x47);
|
||||
segment7[5] = static_cast<char>(0x60);
|
||||
segment7[8] = static_cast<char>(0x24);
|
||||
segment7[9] = static_cast<char>(0x24);
|
||||
out->push_back(7);
|
||||
put_u16(out, 96);
|
||||
out->append(segment7);
|
||||
out->push_back(static_cast<char>(checksum(segment7)));
|
||||
|
||||
std::string segment9(248, '\0');
|
||||
const std::string creator = "Created with EAC3JOC";
|
||||
const std::string renderer = "EAC3JOC Python Renderer";
|
||||
std::memcpy(&segment9[0], creator.data(), creator.size());
|
||||
std::memcpy(&segment9[32], renderer.data(), renderer.size());
|
||||
segment9[96] = 2;
|
||||
segment9[97] = 1;
|
||||
segment9[98] = 0;
|
||||
segment9[103] = 0x03;
|
||||
segment9[106] = 0x01;
|
||||
segment9[111] = 0x22;
|
||||
segment9[112] = static_cast<char>(0xFF);
|
||||
out->push_back(9);
|
||||
put_u16(out, 248);
|
||||
out->append(segment9);
|
||||
out->push_back(static_cast<char>(checksum(segment9)));
|
||||
|
||||
// The reference allocates the body zeroed and then fills only the trailing
|
||||
// `object_count` bytes with 0x84, so the template region stays zero.
|
||||
const std::size_t object_body = 5u + 262u + object_count;
|
||||
std::string segment10(object_body, '\0');
|
||||
const std::uint32_t sync = 0xF8726FBDu;
|
||||
std::memcpy(&segment10[0], &sync, 4);
|
||||
segment10[4] = static_cast<char>(object_count);
|
||||
for (std::size_t i = 5u + 262u; i < segment10.size(); ++i) {
|
||||
segment10[i] = static_cast<char>(0x84);
|
||||
}
|
||||
const std::size_t object_modes = 4u + 2u + 1u + 9u * 15u + object_count;
|
||||
for (std::uint32_t i = 10; i < std::min<std::uint32_t>(object_count, 10u + kObjectCount); ++i) {
|
||||
const std::size_t index = object_modes + i;
|
||||
if (index >= segment10.size()) {
|
||||
return Status::fail(JOC_ERR_INTERNAL, stage::kOutput, "dbmd object slot out of range");
|
||||
}
|
||||
segment10[index] = static_cast<char>((static_cast<unsigned char>(segment10[index]) & 0xF8u) |
|
||||
mode_value);
|
||||
}
|
||||
out->push_back(10);
|
||||
put_u16(out, static_cast<std::uint16_t>(segment10.size()));
|
||||
out->append(segment10);
|
||||
out->push_back(static_cast<char>(checksum(segment10)));
|
||||
out->append("\0\0", 2);
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
std::string build_axml(const std::vector<Track>& tracks, double duration_sec, std::uint32_t rate) {
|
||||
const double scale = static_cast<double>(rate);
|
||||
std::string out;
|
||||
out.reserve(64u * 1024u);
|
||||
const std::string duration_ts = ts(duration_sec);
|
||||
|
||||
out += "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
|
||||
out += "<ebuCoreMain xsi:schemaLocation=\"urn:ebu:metadata-schema:ebuCore_2016 ebucore.xsd\" "
|
||||
"lang=\"en\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
|
||||
"xmlns=\"urn:ebu:metadata-schema:ebuCore_2016\">";
|
||||
out += "<coreMetadata><format><audioFormatExtended>";
|
||||
out += "<audioProgramme audioProgrammeID=\"APR_1001\" audioProgrammeName=\"EAC3JOC_Export\" "
|
||||
"start=\"" +
|
||||
ts(0.0) + "\" end=\"" + duration_ts + "\">";
|
||||
out += "<audioContentIDRef>ACO_1001</audioContentIDRef>";
|
||||
out += "<audioContentIDRef>ACO_1002</audioContentIDRef>";
|
||||
out += "</audioProgramme>";
|
||||
out += "<audioContent audioContentID=\"ACO_1001\" "
|
||||
"audioContentName=\"EAC3JOC_Master_Content\">";
|
||||
out += "<audioObjectIDRef>AO_1001</audioObjectIDRef>";
|
||||
out += "<dialogue mixedContentKind=\"0\">2</dialogue>";
|
||||
out += "</audioContent>";
|
||||
out += "<audioContent audioContentID=\"ACO_1002\" audioContentName=\"Objects\">";
|
||||
for (std::uint32_t i = 0; i < kObjectCount; ++i) {
|
||||
out += "<audioObjectIDRef>AO_" + hex4(0x100b + i) + "</audioObjectIDRef>";
|
||||
}
|
||||
out += "<dialogue mixedContentKind=\"0\">2</dialogue>";
|
||||
out += "</audioContent>";
|
||||
out += "<audioObject audioObjectID=\"AO_1001\" audioObjectName=\"Bed\" start=\"" + ts(0.0) +
|
||||
"\" duration=\"" + duration_ts + "\">";
|
||||
out += "<audioPackFormatIDRef>AP_00011001</audioPackFormatIDRef>";
|
||||
for (std::uint32_t i = 0; i < 10; ++i) {
|
||||
out += "<audioTrackUIDRef>ATU_" + hex8(i + 1) + "</audioTrackUIDRef>";
|
||||
}
|
||||
out += "</audioObject>";
|
||||
for (std::uint32_t i = 0; i < kObjectCount; ++i) {
|
||||
out += "<audioObject audioObjectID=\"AO_" + hex4(0x100b + i) +
|
||||
"\" audioObjectName=\"Audio Object " + std::to_string(i + 1) + "\" start=\"" +
|
||||
ts(0.0) + "\" duration=\"" + duration_ts + "\">";
|
||||
out += "<audioPackFormatIDRef>AP_0003" + hex4(0x1001 + i) + "</audioPackFormatIDRef>";
|
||||
out += "<audioTrackUIDRef>ATU_" + hex8(11 + i) + "</audioTrackUIDRef>";
|
||||
out += "</audioObject>";
|
||||
}
|
||||
out += "<audioPackFormat audioPackFormatID=\"AP_00011001\" "
|
||||
"audioPackFormatName=\"EAC3JOCBedPack\" typeDefinition=\"DirectSpeakers\" "
|
||||
"typeLabel=\"0001\">";
|
||||
for (std::uint32_t i = 0; i < 10; ++i) {
|
||||
out += "<audioChannelFormatIDRef>AC_0001" + hex4(0x1001 + i) +
|
||||
"</audioChannelFormatIDRef>";
|
||||
}
|
||||
out += "</audioPackFormat>";
|
||||
for (std::uint32_t i = 0; i < kObjectCount; ++i) {
|
||||
out += "<audioPackFormat audioPackFormatID=\"AP_0003" + hex4(0x1001 + i) +
|
||||
"\" audioPackFormatName=\"JOC_Object_" + std::to_string(i + 1) +
|
||||
"\" typeDefinition=\"Objects\" typeLabel=\"0003\">";
|
||||
out += "<audioChannelFormatIDRef>AC_0003" + hex4(0x1001 + i) +
|
||||
"</audioChannelFormatIDRef>";
|
||||
out += "</audioPackFormat>";
|
||||
}
|
||||
for (std::uint32_t i = 0; i < 10; ++i) {
|
||||
out += "<audioChannelFormat audioChannelFormatID=\"AC_0001" + hex4(0x1001 + i) +
|
||||
"\" audioChannelFormatName=\"" + kBedNames[i] +
|
||||
"\" typeDefinition=\"DirectSpeakers\" typeLabel=\"0001\">";
|
||||
out += "<audioBlockFormat audioBlockFormatID=\"AB_0001" + hex4(0x1001 + i) +
|
||||
"_00000001\">";
|
||||
out += "<cartesian>1</cartesian>";
|
||||
out += "<position coordinate=\"X\">" + pynum::format_fixed(kBedPos[i][0], 10) +
|
||||
"</position>";
|
||||
out += "<position coordinate=\"Y\">" + pynum::format_fixed(kBedPos[i][1], 10) +
|
||||
"</position>";
|
||||
if (kBedPos[i][2] != 0.0) {
|
||||
out += "<position coordinate=\"Z\">" + pynum::format_fixed(kBedPos[i][2], 10) +
|
||||
"</position>";
|
||||
}
|
||||
out += std::string("<speakerLabel>") + kBedLabels[i] + "</speakerLabel>";
|
||||
out += "</audioBlockFormat>";
|
||||
out += "</audioChannelFormat>";
|
||||
}
|
||||
for (std::size_t i = 0; i < tracks.size(); ++i) {
|
||||
const Track& track = tracks[i];
|
||||
out += "<audioChannelFormat audioChannelFormatID=\"AC_0003" +
|
||||
hex4(0x1001 + static_cast<std::uint32_t>(i)) + "\" audioChannelFormatName=\"" +
|
||||
track.name + "\" typeDefinition=\"Objects\" typeLabel=\"0003\">";
|
||||
for (std::size_t k = 0; k < track.blocks.size(); ++k) {
|
||||
const Keyframe& block = track.blocks[k];
|
||||
out += "<audioBlockFormat audioBlockFormatID=\"AB_0003" +
|
||||
hex4(0x1001 + static_cast<std::uint32_t>(i)) + "_" +
|
||||
hex8(static_cast<std::uint32_t>(k + 1)) + "\" rtime=\"" +
|
||||
ts(static_cast<double>(block.rtime_samples) / scale) + "\" duration=\"" +
|
||||
ts(static_cast<double>(block.duration_samples) / scale) + "\">";
|
||||
out += "<cartesian>1</cartesian>";
|
||||
out += "<position coordinate=\"X\">" + pynum::format_fixed(block.x, 10) + "</position>";
|
||||
out += "<position coordinate=\"Y\">" + pynum::format_fixed(block.y, 10) + "</position>";
|
||||
if (block.z != 0.0) {
|
||||
out += "<position coordinate=\"Z\">" + pynum::format_fixed(block.z, 10) +
|
||||
"</position>";
|
||||
}
|
||||
out += "<jumpPosition interpolationLength=\"" +
|
||||
pynum::format_fixed(static_cast<double>(block.interpolation_samples) / scale,
|
||||
5) +
|
||||
"\">1</jumpPosition>";
|
||||
out += "</audioBlockFormat>";
|
||||
}
|
||||
out += "</audioChannelFormat>";
|
||||
}
|
||||
for (std::uint32_t i = 0; i < 10; ++i) {
|
||||
out += "<audioTrackUID UID=\"ATU_" + hex8(i + 1) +
|
||||
"\" bitDepth=\"24\" sampleRate=\"48000\">";
|
||||
out += "<audioTrackFormatIDRef>AT_0001" + hex4(0x1001 + i) + "_01</audioTrackFormatIDRef>";
|
||||
out += "<audioPackFormatIDRef>AP_00011001</audioPackFormatIDRef>";
|
||||
out += "</audioTrackUID>";
|
||||
}
|
||||
for (std::uint32_t i = 0; i < kObjectCount; ++i) {
|
||||
out += "<audioTrackUID UID=\"ATU_" + hex8(11 + i) +
|
||||
"\" bitDepth=\"24\" sampleRate=\"48000\">";
|
||||
out += "<audioTrackFormatIDRef>AT_0003" + hex4(0x1001 + i) + "_01</audioTrackFormatIDRef>";
|
||||
out += "<audioPackFormatIDRef>AP_0003" + hex4(0x1001 + i) + "</audioPackFormatIDRef>";
|
||||
out += "</audioTrackUID>";
|
||||
}
|
||||
for (std::uint32_t i = 0; i < 10; ++i) {
|
||||
out += "<audioTrackFormat audioTrackFormatID=\"AT_0001" + hex4(0x1001 + i) +
|
||||
"_01\" audioTrackFormatName=\"PCM_" + kBedNames[i] +
|
||||
"\" formatDefinition=\"PCM\" formatLabel=\"0001\">";
|
||||
out += "<audioStreamFormatIDRef>AS_0001" + hex4(0x1001 + i) +
|
||||
"</audioStreamFormatIDRef>";
|
||||
out += "</audioTrackFormat>";
|
||||
}
|
||||
for (std::uint32_t i = 0; i < kObjectCount; ++i) {
|
||||
out += "<audioTrackFormat audioTrackFormatID=\"AT_0003" + hex4(0x1001 + i) +
|
||||
"_01\" audioTrackFormatName=\"PCM_JOC_Object_" + std::to_string(i + 1) +
|
||||
"\" formatDefinition=\"PCM\" formatLabel=\"0001\">";
|
||||
out += "<audioStreamFormatIDRef>AS_0003" + hex4(0x1001 + i) +
|
||||
"</audioStreamFormatIDRef>";
|
||||
out += "</audioTrackFormat>";
|
||||
}
|
||||
for (std::uint32_t i = 0; i < 10; ++i) {
|
||||
out += "<audioStreamFormat audioStreamFormatID=\"AS_0001" + hex4(0x1001 + i) +
|
||||
"\" audioStreamFormatName=\"PCM_" + kBedNames[i] +
|
||||
"\" formatDefinition=\"PCM\" formatLabel=\"0001\">";
|
||||
out += "<audioChannelFormatIDRef>AC_0001" + hex4(0x1001 + i) +
|
||||
"</audioChannelFormatIDRef>";
|
||||
out += "<audioPackFormatIDRef>AP_00011001</audioPackFormatIDRef>";
|
||||
out += "<audioTrackFormatIDRef>AT_0001" + hex4(0x1001 + i) +
|
||||
"_01</audioTrackFormatIDRef>";
|
||||
out += "</audioStreamFormat>";
|
||||
}
|
||||
for (std::uint32_t i = 0; i < kObjectCount; ++i) {
|
||||
out += "<audioStreamFormat audioStreamFormatID=\"AS_0003" + hex4(0x1001 + i) +
|
||||
"\" audioStreamFormatName=\"PCM_JOC_Object_" + std::to_string(i + 1) +
|
||||
"\" formatDefinition=\"PCM\" formatLabel=\"0001\">";
|
||||
out += "<audioChannelFormatIDRef>AC_0003" + hex4(0x1001 + i) +
|
||||
"</audioChannelFormatIDRef>";
|
||||
out += "<audioPackFormatIDRef>AP_0003" + hex4(0x1001 + i) + "</audioPackFormatIDRef>";
|
||||
out += "<audioTrackFormatIDRef>AT_0003" + hex4(0x1001 + i) +
|
||||
"_01</audioTrackFormatIDRef>";
|
||||
out += "</audioStreamFormat>";
|
||||
}
|
||||
out += "</audioFormatExtended></format></coreMetadata>";
|
||||
out += "</ebuCoreMain>";
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace joc::adm
|
||||
@@ -0,0 +1,43 @@
|
||||
// Port of src/adm_atmos.py.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "foundation/status.h"
|
||||
|
||||
namespace joc::adm {
|
||||
|
||||
inline constexpr int kObjectCount = 15;
|
||||
inline constexpr std::uint32_t kTrackCount = 25;
|
||||
|
||||
struct Keyframe {
|
||||
std::int64_t rtime_samples = 0;
|
||||
double x = 0.0;
|
||||
double y = 0.0;
|
||||
double z = 0.0;
|
||||
std::int64_t duration_samples = 0;
|
||||
std::int64_t interpolation_samples = 0;
|
||||
};
|
||||
|
||||
struct Track {
|
||||
std::string name;
|
||||
std::vector<Keyframe> blocks;
|
||||
};
|
||||
|
||||
// HH:MM:SS.fffff with the reference's truncation + round-half-even carry.
|
||||
std::string ts(double seconds);
|
||||
|
||||
enum class BinauralMode : std::uint32_t { Off = 0, Near = 1, Far = 2, Mid = 3, Unspecified = 4 };
|
||||
|
||||
bool binaural_mode_from_name(const char* name, BinauralMode* out);
|
||||
|
||||
std::string build_axml(const std::vector<Track>& tracks, double duration_sec, std::uint32_t rate);
|
||||
|
||||
std::string build_chna();
|
||||
|
||||
Status build_dbmd(std::uint32_t object_count, BinauralMode mode, std::string* out);
|
||||
|
||||
} // namespace joc::adm
|
||||
@@ -0,0 +1,281 @@
|
||||
#include "adm/adm_tracks.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "foundation/geometry.h"
|
||||
|
||||
namespace joc::adm {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Point {
|
||||
std::int64_t sample = 0;
|
||||
double x = 0.0;
|
||||
double y = 0.0;
|
||||
double z = 0.0;
|
||||
std::int64_t interpolation_samples = 0;
|
||||
};
|
||||
|
||||
// otherwise (the reference drops silently).
|
||||
void append_point(std::vector<Point>* points, std::int64_t sample, double x, double y, double z,
|
||||
std::int64_t interpolation_samples) {
|
||||
if (!points->empty() && sample == points->back().sample) {
|
||||
points->back() = Point{sample, x, y, z, interpolation_samples};
|
||||
} else if (points->empty() || sample > points->back().sample) {
|
||||
points->push_back(Point{sample, x, y, z, interpolation_samples});
|
||||
}
|
||||
}
|
||||
|
||||
void lerp(double ax, double ay, double az, double bx, double by, double bz, double amount,
|
||||
double* x, double* y, double* z) {
|
||||
*x = ax + (bx - ax) * amount;
|
||||
*y = ay + (by - ay) * amount;
|
||||
*z = az + (bz - az) * amount;
|
||||
}
|
||||
|
||||
void points_to_blocks(const std::vector<Point>& points, std::int64_t total_samples,
|
||||
std::vector<Keyframe>* out) {
|
||||
for (std::size_t index = 0; index < points.size(); ++index) {
|
||||
const Point& point = points[index];
|
||||
const std::int64_t end =
|
||||
(index + 1 < points.size()) ? points[index + 1].sample : total_samples;
|
||||
const std::int64_t duration = std::max<std::int64_t>(0, end - point.sample);
|
||||
if (duration == 0) {
|
||||
continue;
|
||||
}
|
||||
Keyframe keyframe;
|
||||
keyframe.rtime_samples = point.sample;
|
||||
keyframe.x = point.x;
|
||||
keyframe.y = point.y;
|
||||
keyframe.z = point.z;
|
||||
keyframe.duration_samples = duration;
|
||||
keyframe.interpolation_samples = std::min(point.interpolation_samples, duration);
|
||||
out->push_back(keyframe);
|
||||
}
|
||||
}
|
||||
|
||||
Status non_monotonic(const char* name, const char* message, int object_index, std::int64_t sample,
|
||||
std::int64_t previous_sample) {
|
||||
return Status::fail(JOC_ERR_OAMD_UNSUPPORTED_VARIANT, stage::kOamd,
|
||||
std::string(name) + ": " + message + " (object " +
|
||||
std::to_string(object_index) + ", sample " + std::to_string(sample) +
|
||||
", previous " + std::to_string(previous_sample) + ")");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status expand_compact(const std::vector<OamdEvent>& events, std::int64_t total_samples, std::uint32_t rate,
|
||||
std::int64_t update_quantum_samples, std::int64_t object_delay_samples,
|
||||
int object_index, std::vector<Keyframe>* out) {
|
||||
out->clear();
|
||||
const double scale = static_cast<double>(rate);
|
||||
if (events.empty()) {
|
||||
Keyframe keyframe;
|
||||
keyframe.rtime_samples = 0;
|
||||
keyframe.duration_samples = total_samples;
|
||||
keyframe.interpolation_samples = 0;
|
||||
keyframe.x = 0.0;
|
||||
keyframe.y = 0.0;
|
||||
keyframe.z = 0.0;
|
||||
out->push_back(keyframe);
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
std::vector<Point> points;
|
||||
double current[3] = {events[0].x, events[0].y, events[0].z};
|
||||
append_point(&points, 0, current[0], current[1], current[2], 0);
|
||||
|
||||
for (std::size_t index = 1; index < events.size(); ++index) {
|
||||
const OamdEvent& event = events[index];
|
||||
const std::int64_t event_start = event.sample + object_delay_samples;
|
||||
if (event_start >= total_samples) {
|
||||
break;
|
||||
}
|
||||
const std::int64_t effective_ramp =
|
||||
std::max<std::int64_t>(0, event.ramp_samples - update_quantum_samples);
|
||||
const std::int64_t block_start =
|
||||
event_start + (effective_ramp != 0 ? update_quantum_samples : 0);
|
||||
if (block_start >= total_samples) {
|
||||
break;
|
||||
}
|
||||
const std::int64_t ramp_end = block_start + effective_ramp;
|
||||
|
||||
if (block_start < points.back().sample) {
|
||||
return non_monotonic("non_monotonic_compact_position_updates",
|
||||
"compact object position update moved backwards", object_index,
|
||||
block_start, points.back().sample);
|
||||
}
|
||||
if (index + 1 < events.size()) {
|
||||
const std::int64_t next_event_start = events[index + 1].sample + object_delay_samples;
|
||||
const std::int64_t next_effective =
|
||||
std::max<std::int64_t>(0, events[index + 1].ramp_samples - update_quantum_samples);
|
||||
const std::int64_t next_block_start =
|
||||
next_event_start + (next_effective != 0 ? update_quantum_samples : 0);
|
||||
if (next_block_start < ramp_end) {
|
||||
return non_monotonic("overlapping_compact_position_ramps",
|
||||
"a new position update arrived before the previous compact "
|
||||
"ramp finished",
|
||||
object_index, block_start, ramp_end);
|
||||
}
|
||||
}
|
||||
|
||||
double target[3] = {event.x, event.y, event.z};
|
||||
std::int64_t interpolation = effective_ramp;
|
||||
const std::int64_t available = total_samples - block_start;
|
||||
if (effective_ramp > available) {
|
||||
const double amount =
|
||||
static_cast<double>(available) / static_cast<double>(effective_ramp);
|
||||
double x = 0.0;
|
||||
double y = 0.0;
|
||||
double z = 0.0;
|
||||
lerp(current[0], current[1], current[2], event.x, event.y, event.z, amount, &x, &y, &z);
|
||||
target[0] = x;
|
||||
target[1] = y;
|
||||
target[2] = z;
|
||||
interpolation = available;
|
||||
}
|
||||
append_point(&points, block_start, target[0], target[1], target[2], interpolation);
|
||||
current[0] = event.x;
|
||||
current[1] = event.y;
|
||||
current[2] = event.z;
|
||||
}
|
||||
|
||||
points_to_blocks(points, total_samples, out);
|
||||
(void)scale;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status expand_dense64(const std::vector<OamdEvent>& events, std::int64_t total_samples, std::uint32_t rate,
|
||||
std::int64_t update_quantum_samples, std::int64_t object_delay_samples,
|
||||
int object_index, std::vector<Keyframe>* out) {
|
||||
out->clear();
|
||||
(void)rate;
|
||||
if (events.empty()) {
|
||||
Keyframe keyframe;
|
||||
keyframe.duration_samples = total_samples;
|
||||
out->push_back(keyframe);
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
std::vector<Point> points;
|
||||
double current[3] = {events[0].x, events[0].y, events[0].z};
|
||||
append_point(&points, 0, current[0], current[1], current[2], 0);
|
||||
|
||||
for (std::size_t index = 1; index < events.size(); ++index) {
|
||||
const OamdEvent& event = events[index];
|
||||
const std::int64_t start = event.sample + object_delay_samples;
|
||||
if (start >= total_samples) {
|
||||
break;
|
||||
}
|
||||
if (start < points.back().sample) {
|
||||
return non_monotonic("non_monotonic_position_updates",
|
||||
"object position update moved backwards", object_index, start,
|
||||
points.back().sample);
|
||||
}
|
||||
if (start > points.back().sample) {
|
||||
append_point(&points, start, current[0], current[1], current[2], 0);
|
||||
}
|
||||
const std::int64_t effective_ramp =
|
||||
std::max<std::int64_t>(0, event.ramp_samples - update_quantum_samples);
|
||||
if (effective_ramp == 0) {
|
||||
append_point(&points, start, event.x, event.y, event.z, 0);
|
||||
current[0] = event.x;
|
||||
current[1] = event.y;
|
||||
current[2] = event.z;
|
||||
continue;
|
||||
}
|
||||
const std::int64_t steps =
|
||||
(effective_ramp + update_quantum_samples - 1) / update_quantum_samples;
|
||||
const std::int64_t end = start + steps * update_quantum_samples;
|
||||
if (index + 1 < events.size()) {
|
||||
const std::int64_t next_start = events[index + 1].sample + object_delay_samples;
|
||||
if (next_start < end) {
|
||||
return non_monotonic("overlapping_position_ramps",
|
||||
"a new position update arrived before the previous ramp "
|
||||
"finished",
|
||||
object_index, start, end);
|
||||
}
|
||||
}
|
||||
std::int64_t future = effective_ramp;
|
||||
std::int64_t elapsed = 0;
|
||||
double position[3] = {current[0], current[1], current[2]};
|
||||
while (future > 0) {
|
||||
const double amount =
|
||||
std::min(static_cast<double>(update_quantum_samples) / static_cast<double>(future),
|
||||
1.0);
|
||||
double x = 0.0;
|
||||
double y = 0.0;
|
||||
double z = 0.0;
|
||||
lerp(position[0], position[1], position[2], event.x, event.y, event.z, amount, &x, &y,
|
||||
&z);
|
||||
position[0] = x;
|
||||
position[1] = y;
|
||||
position[2] = z;
|
||||
elapsed += update_quantum_samples;
|
||||
const std::int64_t sample = start + elapsed;
|
||||
if (sample >= total_samples) {
|
||||
break;
|
||||
}
|
||||
append_point(&points, sample, position[0], position[1], position[2],
|
||||
update_quantum_samples);
|
||||
future -= update_quantum_samples;
|
||||
}
|
||||
current[0] = event.x;
|
||||
current[1] = event.y;
|
||||
current[2] = event.z;
|
||||
}
|
||||
|
||||
points_to_blocks(points, total_samples, out);
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
void TrajectoryBuilder::submit_frame(std::int64_t frame_index, const oamd::OamdUpdate* update,
|
||||
std::int64_t outer_offset) {
|
||||
std::int64_t event_sample = frame_index * 1536;
|
||||
std::int64_t ramp_samples = 0;
|
||||
if (update != nullptr) {
|
||||
state_.apply(*update);
|
||||
event_sample += outer_offset + static_cast<std::int64_t>(update->block_offset_samples);
|
||||
ramp_samples = static_cast<std::int64_t>(update->ramp_duration_samples);
|
||||
}
|
||||
for (int object = 1; object <= kObjectCount; ++object) {
|
||||
double x = 0.0;
|
||||
double y = 0.0;
|
||||
double z = 0.0;
|
||||
geometry::q_to_adm_xyz(state_.q(object, 0), state_.q(object, 1), state_.q(object, 2), &x,
|
||||
&y, &z);
|
||||
const int slot = object - 1;
|
||||
if (!has_previous_[slot] || previous_[slot][0] != x || previous_[slot][1] != y ||
|
||||
previous_[slot][2] != z) {
|
||||
events_[slot].push_back(OamdEvent{event_sample, x, y, z, ramp_samples});
|
||||
previous_[slot][0] = x;
|
||||
previous_[slot][1] = y;
|
||||
previous_[slot][2] = z;
|
||||
has_previous_[slot] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Status TrajectoryBuilder::build(std::int64_t total_samples, TrajectoryMode mode,
|
||||
std::vector<Track>* out) const {
|
||||
out->clear();
|
||||
out->reserve(kObjectCount);
|
||||
for (int object = 1; object <= kObjectCount; ++object) {
|
||||
Track track;
|
||||
track.name = "JOC_Object_" + std::to_string(object);
|
||||
const Status status =
|
||||
(mode == TrajectoryMode::Compact)
|
||||
? expand_compact(events_[object - 1], total_samples, rate_, quantum_,
|
||||
object_delay_, object, &track.blocks)
|
||||
: expand_dense64(events_[object - 1], total_samples, rate_, quantum_,
|
||||
object_delay_, object, &track.blocks);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
out->push_back(std::move(track));
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
} // namespace joc::adm
|
||||
@@ -0,0 +1,61 @@
|
||||
// Port of src/oamd_tracks.py.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "adm/adm_metadata.h"
|
||||
#include "foundation/status.h"
|
||||
#include "oamd/oamd_parser.h"
|
||||
|
||||
namespace joc::adm {
|
||||
|
||||
struct OamdEvent {
|
||||
std::int64_t sample = 0;
|
||||
double x = 0.0;
|
||||
double y = 0.0;
|
||||
double z = 0.0;
|
||||
std::int64_t ramp_samples = 0;
|
||||
};
|
||||
|
||||
enum class TrajectoryMode { Compact, Dense64 };
|
||||
|
||||
// Feeds the same per-frame OAMD state machine the reference's build_adm_tracks
|
||||
// runs, and records one event per object whenever its coordinates change.
|
||||
class TrajectoryBuilder {
|
||||
public:
|
||||
TrajectoryBuilder(std::uint32_t rate = 48000, std::int64_t update_quantum_samples = 64,
|
||||
std::int64_t object_delay_samples = 1473)
|
||||
: rate_(rate),
|
||||
quantum_(update_quantum_samples),
|
||||
object_delay_(object_delay_samples) {}
|
||||
|
||||
void submit_frame(std::int64_t frame_index, const oamd::OamdUpdate* update, std::int64_t outer_offset);
|
||||
|
||||
Status build(std::int64_t total_samples, TrajectoryMode mode, std::vector<Track>* out) const;
|
||||
|
||||
const std::vector<OamdEvent>& events(int object_index) const { return events_[object_index]; }
|
||||
std::uint32_t rate() const { return rate_; }
|
||||
std::int64_t object_delay_samples() const { return object_delay_; }
|
||||
|
||||
private:
|
||||
std::uint32_t rate_;
|
||||
std::int64_t quantum_;
|
||||
std::int64_t object_delay_;
|
||||
oamd::OamdState state_;
|
||||
std::vector<OamdEvent> events_[kObjectCount];
|
||||
bool has_previous_[kObjectCount] = {};
|
||||
double previous_[kObjectCount][3] = {};
|
||||
};
|
||||
|
||||
Status expand_compact(const std::vector<OamdEvent>& events, std::int64_t total_samples, std::uint32_t rate,
|
||||
std::int64_t update_quantum_samples, std::int64_t object_delay_samples,
|
||||
int object_index, std::vector<Keyframe>* out);
|
||||
|
||||
Status expand_dense64(const std::vector<OamdEvent>& events, std::int64_t total_samples, std::uint32_t rate,
|
||||
std::int64_t update_quantum_samples, std::int64_t object_delay_samples,
|
||||
int object_index, std::vector<Keyframe>* out);
|
||||
|
||||
} // namespace joc::adm
|
||||
@@ -0,0 +1,249 @@
|
||||
|
||||
#include "joc_core.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "eac3_transport/eac3_reader.h"
|
||||
#include "emdf/emdf_parser.h"
|
||||
#include "foundation/status.h"
|
||||
#include "joc_bitstream/joc_parser.h"
|
||||
|
||||
namespace {
|
||||
|
||||
thread_local std::string g_detail;
|
||||
|
||||
joc_error finish(const joc::Status& status) {
|
||||
if (status.ok()) {
|
||||
g_detail.clear();
|
||||
return JOC_OK;
|
||||
}
|
||||
g_detail.assign(status.stage());
|
||||
g_detail.append(": ");
|
||||
g_detail.append(status.message());
|
||||
return status.code();
|
||||
}
|
||||
|
||||
joc_error arg_fail(const char* message) {
|
||||
return finish(joc::Status::fail(JOC_ERR_INVALID_ARGUMENT, "core", message));
|
||||
}
|
||||
|
||||
void fill_emdf_info(const joc::emdf::Container& container, joc_emdf_info* out) {
|
||||
std::memset(out, 0, sizeof(*out));
|
||||
out->struct_size = sizeof(joc_emdf_info);
|
||||
out->struct_version = JOC_EMDF_INFO_VERSION;
|
||||
out->start_bit = static_cast<std::uint32_t>(container.start_bit);
|
||||
out->container_bytes = static_cast<std::uint32_t>(container.raw_size);
|
||||
out->payload_count = static_cast<std::uint32_t>(container.payload_count);
|
||||
for (std::size_t i = 0; i < container.payload_count; ++i) {
|
||||
out->payloads[i].id = container.payloads[i].id;
|
||||
out->payloads[i].sample_offset = container.payloads[i].sample_offset;
|
||||
out->payloads[i].bit_offset = static_cast<std::uint32_t>(container.payloads[i].bit_offset);
|
||||
out->payloads[i].size = static_cast<std::uint32_t>(container.payloads[i].size);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
std::uint32_t JOC_CALL joc_abi_version(void) { return JOC_ABI_VERSION; }
|
||||
|
||||
const char* JOC_CALL joc_version_string(void) { return "0.1.0-m1"; }
|
||||
|
||||
std::uint32_t JOC_CALL joc_event_size(void) { return static_cast<std::uint32_t>(sizeof(joc_event)); }
|
||||
std::uint32_t JOC_CALL joc_task_config_size(void) {
|
||||
return static_cast<std::uint32_t>(sizeof(joc_task_config));
|
||||
}
|
||||
std::uint32_t JOC_CALL joc_task_result_size(void) {
|
||||
return static_cast<std::uint32_t>(sizeof(joc_task_result));
|
||||
}
|
||||
|
||||
const char* JOC_CALL joc_build_info(void) {
|
||||
static const std::string info = [] {
|
||||
std::string text = "joc_core 0.1.0-m1 (";
|
||||
#if defined(_MSC_VER)
|
||||
text += "msvc " + std::to_string(_MSC_VER);
|
||||
#elif defined(__clang__)
|
||||
text += std::string("clang ") + __clang_version__;
|
||||
#elif defined(__GNUC__)
|
||||
text += "gcc " + std::to_string(__GNUC__) + "." + std::to_string(__GNUC_MINOR__);
|
||||
#else
|
||||
text += "unknown-compiler";
|
||||
#endif
|
||||
#if defined(_M_AMD64) || defined(__x86_64__)
|
||||
text += ", x64";
|
||||
#elif defined(_M_ARM64) || defined(__aarch64__)
|
||||
text += ", arm64";
|
||||
#elif defined(_M_IX86) || defined(__i386__)
|
||||
text += ", x86";
|
||||
#endif
|
||||
text += ", c++";
|
||||
text += std::to_string(static_cast<long long>(__cplusplus / 100 % 100));
|
||||
text += ")";
|
||||
return text;
|
||||
}();
|
||||
return info.c_str();
|
||||
}
|
||||
|
||||
const char* JOC_CALL joc_error_name(joc_error code) {
|
||||
switch (code) {
|
||||
case JOC_OK: return "JOC_OK";
|
||||
case JOC_ERR_INVALID_ARGUMENT: return "JOC_ERR_INVALID_ARGUMENT";
|
||||
case JOC_ERR_INVALID_CONFIG: return "JOC_ERR_INVALID_CONFIG";
|
||||
case JOC_ERR_OUT_OF_MEMORY: return "JOC_ERR_OUT_OF_MEMORY";
|
||||
case JOC_ERR_IO: return "JOC_ERR_IO";
|
||||
case JOC_ERR_UNSUPPORTED_PLATFORM: return "JOC_ERR_UNSUPPORTED_PLATFORM";
|
||||
case JOC_ERR_LIBRARY_MISSING: return "JOC_ERR_LIBRARY_MISSING";
|
||||
case JOC_ERR_INPUT_NOT_FOUND: return "JOC_ERR_INPUT_NOT_FOUND";
|
||||
case JOC_ERR_INPUT_FORMAT: return "JOC_ERR_INPUT_FORMAT";
|
||||
case JOC_ERR_EAC3_SYNCFRAME: return "JOC_ERR_EAC3_SYNCFRAME";
|
||||
case JOC_ERR_EMDF_TRANSPORT: return "JOC_ERR_EMDF_TRANSPORT";
|
||||
case JOC_ERR_EMDF_SYNTAX: return "JOC_ERR_EMDF_SYNTAX";
|
||||
case JOC_ERR_JOC_SYNTAX: return "JOC_ERR_JOC_SYNTAX";
|
||||
case JOC_ERR_JOC_UNSUPPORTED_VARIANT: return "JOC_ERR_JOC_UNSUPPORTED_VARIANT";
|
||||
case JOC_ERR_OAMD_SYNTAX: return "JOC_ERR_OAMD_SYNTAX";
|
||||
case JOC_ERR_OAMD_UNSUPPORTED_VARIANT: return "JOC_ERR_OAMD_UNSUPPORTED_VARIANT";
|
||||
case JOC_ERR_BITSTREAM_TRUNCATED: return "JOC_ERR_BITSTREAM_TRUNCATED";
|
||||
case JOC_ERR_BITSTREAM_PADDING: return "JOC_ERR_BITSTREAM_PADDING";
|
||||
case JOC_ERR_HRTF_NOT_FOUND: return "JOC_ERR_HRTF_NOT_FOUND";
|
||||
case JOC_ERR_HRTF_FORMAT: return "JOC_ERR_HRTF_FORMAT";
|
||||
case JOC_ERR_HRTF_VERSION: return "JOC_ERR_HRTF_VERSION";
|
||||
case JOC_ERR_HRTF_HASH: return "JOC_ERR_HRTF_HASH";
|
||||
case JOC_ERR_HRTF_UNSUPPORTED_CONVENTION: return "JOC_ERR_HRTF_UNSUPPORTED_CONVENTION";
|
||||
case JOC_ERR_LAYOUT_UNSUPPORTED: return "JOC_ERR_LAYOUT_UNSUPPORTED";
|
||||
case JOC_ERR_RENDER_FAILED: return "JOC_ERR_RENDER_FAILED";
|
||||
case JOC_ERR_OUTPUT_OPEN: return "JOC_ERR_OUTPUT_OPEN";
|
||||
case JOC_ERR_OUTPUT_WRITE: return "JOC_ERR_OUTPUT_WRITE";
|
||||
case JOC_ERR_OUTPUT_CLIP_ABORT: return "JOC_ERR_OUTPUT_CLIP_ABORT";
|
||||
case JOC_ERR_ADM_VALIDATION: return "JOC_ERR_ADM_VALIDATION";
|
||||
case JOC_ERR_CANCELLED: return "JOC_ERR_CANCELLED";
|
||||
case JOC_ERR_STATE: return "JOC_ERR_STATE";
|
||||
case JOC_ERR_NOT_SUPPORTED: return "JOC_ERR_NOT_SUPPORTED";
|
||||
case JOC_ERR_INTERNAL: return "JOC_ERR_INTERNAL";
|
||||
default: return "JOC_ERR_UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
const char* JOC_CALL joc_error_stage(joc_error code) {
|
||||
switch (code) {
|
||||
case JOC_ERR_EAC3_SYNCFRAME:
|
||||
case JOC_ERR_INPUT_NOT_FOUND:
|
||||
case JOC_ERR_INPUT_FORMAT:
|
||||
return "eac3_transport";
|
||||
case JOC_ERR_EMDF_TRANSPORT:
|
||||
case JOC_ERR_EMDF_SYNTAX:
|
||||
return "emdf";
|
||||
case JOC_ERR_JOC_SYNTAX:
|
||||
case JOC_ERR_JOC_UNSUPPORTED_VARIANT:
|
||||
return "joc";
|
||||
case JOC_ERR_OAMD_SYNTAX:
|
||||
case JOC_ERR_OAMD_UNSUPPORTED_VARIANT:
|
||||
return "oamd";
|
||||
case JOC_ERR_BITSTREAM_TRUNCATED:
|
||||
case JOC_ERR_BITSTREAM_PADDING:
|
||||
return "bitstream";
|
||||
case JOC_ERR_HRTF_NOT_FOUND:
|
||||
case JOC_ERR_HRTF_FORMAT:
|
||||
case JOC_ERR_HRTF_VERSION:
|
||||
case JOC_ERR_HRTF_HASH:
|
||||
case JOC_ERR_HRTF_UNSUPPORTED_CONVENTION:
|
||||
return "hrtf";
|
||||
case JOC_ERR_LAYOUT_UNSUPPORTED:
|
||||
case JOC_ERR_RENDER_FAILED:
|
||||
return "render";
|
||||
case JOC_ERR_OUTPUT_OPEN:
|
||||
case JOC_ERR_OUTPUT_WRITE:
|
||||
case JOC_ERR_OUTPUT_CLIP_ABORT:
|
||||
case JOC_ERR_ADM_VALIDATION:
|
||||
return "output";
|
||||
case JOC_ERR_CANCELLED:
|
||||
case JOC_ERR_STATE:
|
||||
case JOC_ERR_NOT_SUPPORTED:
|
||||
case JOC_ERR_INTERNAL:
|
||||
return "task";
|
||||
default:
|
||||
return "core";
|
||||
}
|
||||
}
|
||||
|
||||
const char* JOC_CALL joc_last_error_detail(void) { return g_detail.c_str(); }
|
||||
|
||||
joc_error JOC_CALL joc_parse_id14(const std::uint8_t* payload, std::size_t payload_size,
|
||||
joc_frame_params* out_params) {
|
||||
if (payload == nullptr || out_params == nullptr || payload_size == 0) {
|
||||
return arg_fail("joc_parse_id14 requires a non-empty payload and an output struct");
|
||||
}
|
||||
return finish(joc::joc::parse_id14(payload, payload_size, out_params, nullptr));
|
||||
}
|
||||
|
||||
joc_error JOC_CALL joc_parse_eac3_frame(const std::uint8_t* frame, std::size_t frame_size,
|
||||
joc_frame_params* out_params, joc_emdf_info* out_emdf) {
|
||||
if (frame == nullptr || out_params == nullptr || frame_size == 0) {
|
||||
return arg_fail("joc_parse_eac3_frame requires a frame and an output struct");
|
||||
}
|
||||
joc::emdf::Container container;
|
||||
const joc::Status status =
|
||||
joc::joc::parse_eac3_frame(frame, frame_size, out_params, &container, nullptr);
|
||||
if (!status.ok()) {
|
||||
return finish(status);
|
||||
}
|
||||
if (out_emdf != nullptr) {
|
||||
fill_emdf_info(container, out_emdf);
|
||||
}
|
||||
return JOC_OK;
|
||||
}
|
||||
|
||||
joc_error JOC_CALL joc_extract_payload(const std::uint8_t* frame, std::size_t frame_size,
|
||||
const joc_emdf_payload_info* payload, std::uint8_t* out,
|
||||
std::size_t out_capacity, std::size_t* out_size) {
|
||||
if (frame == nullptr || payload == nullptr || out_size == nullptr) {
|
||||
return arg_fail("joc_extract_payload requires frame, payload and out_size");
|
||||
}
|
||||
*out_size = payload->size;
|
||||
if (out == nullptr) {
|
||||
return JOC_OK;
|
||||
}
|
||||
if (out_capacity < payload->size) {
|
||||
return arg_fail("joc_extract_payload output buffer too small");
|
||||
}
|
||||
joc::emdf::Payload entry;
|
||||
entry.id = payload->id;
|
||||
entry.sample_offset = payload->sample_offset;
|
||||
entry.bit_offset = payload->bit_offset;
|
||||
entry.size = payload->size;
|
||||
std::vector<std::uint8_t> bytes;
|
||||
const joc::Status status = joc::emdf::extract_payload_bytes(frame, frame_size, entry, &bytes);
|
||||
if (!status.ok()) {
|
||||
return finish(status);
|
||||
}
|
||||
if (!bytes.empty()) {
|
||||
std::memcpy(out, bytes.data(), bytes.size());
|
||||
}
|
||||
*out_size = bytes.size();
|
||||
return JOC_OK;
|
||||
}
|
||||
|
||||
joc_error JOC_CALL joc_eac3_frame_bytes(const std::uint8_t* data, std::size_t size, std::size_t offset,
|
||||
std::size_t* out_frame_bytes) {
|
||||
if (data == nullptr || out_frame_bytes == nullptr) {
|
||||
return arg_fail("joc_eac3_frame_bytes requires data and out_frame_bytes");
|
||||
}
|
||||
const joc_error code = joc::eac3::FrameReader::frame_bytes(data, size, offset, out_frame_bytes);
|
||||
if (code != JOC_OK) {
|
||||
return finish(joc::Status::fail(code, joc::stage::kEac3,
|
||||
"invalid or truncated E-AC-3 syncframe at byte " +
|
||||
std::to_string(offset)));
|
||||
}
|
||||
return JOC_OK;
|
||||
}
|
||||
|
||||
joc_error JOC_CALL joc_check_id14_padding(const std::uint8_t* payload, std::size_t payload_size,
|
||||
std::uint32_t* out_trailing_bits) {
|
||||
if (payload == nullptr || payload_size == 0) {
|
||||
return arg_fail("joc_check_id14_padding requires a payload");
|
||||
}
|
||||
return finish(joc::joc::check_id14_padding(payload, payload_size, out_trailing_bits));
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,162 @@
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "foundation/status.h"
|
||||
#include "joc_core.h"
|
||||
#include "joc_stream.h"
|
||||
#include "stream/stream.h"
|
||||
|
||||
struct joc_stream {
|
||||
joc::stream::Stream instance;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
joc_error finish_stream(const joc::Status& status) {
|
||||
return status.code();
|
||||
}
|
||||
|
||||
joc::stream::Config to_config(const joc_stream_config& config) {
|
||||
joc::stream::Config out;
|
||||
out.input = config.input;
|
||||
out.output = config.output;
|
||||
if (config.speaker_layout_name != nullptr) { out.layout = config.speaker_layout_name; }
|
||||
if (config.speaker_metadata_offset != 0u) {
|
||||
out.metadata_offset = config.speaker_metadata_offset;
|
||||
}
|
||||
out.binaural_mode = config.binaural_mode != 0u ? config.binaural_mode : JOC_BINAURAL_MID;
|
||||
if (config.hrtf_path != nullptr) { out.hrtf_path = config.hrtf_path; }
|
||||
if (config.kernels_path != nullptr) { out.kernels_path = config.kernels_path; }
|
||||
if (config.binaural_tail_seconds > 0.0) { out.tail_seconds = config.binaural_tail_seconds; }
|
||||
if (config.object_delay_samples != 0u) {
|
||||
out.object_delay_samples = config.object_delay_samples;
|
||||
}
|
||||
out.gain_db = config.gain_db;
|
||||
out.native_threads = config.native_threads;
|
||||
// The binaural HRTF inputs of joc_task_config, copied with the same defaults:
|
||||
// the policy is taken verbatim (0 is "none", a real choice, not "unset") and
|
||||
// the radius keeps its documented default of 1.0 when the field is not set.
|
||||
if (config.hrtf_sofa_path != nullptr) { out.hrtf_sofa_path = config.hrtf_sofa_path; }
|
||||
if (config.personalized_headphone_path != nullptr) {
|
||||
out.personalized_headphone_path = config.personalized_headphone_path;
|
||||
}
|
||||
if (config.hrtf_cache_dir != nullptr) { out.hrtf_cache_dir = config.hrtf_cache_dir; }
|
||||
out.hrtf_cache_policy = config.hrtf_cache_policy;
|
||||
if (config.hrtf_radius_m > 0.0) { out.hrtf_radius_m = config.hrtf_radius_m; }
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
joc_error JOC_CALL joc_stream_create(const joc_stream_config* config, joc_stream** out) {
|
||||
if (config == nullptr || out == nullptr || config->struct_size != sizeof(joc_stream_config)) {
|
||||
return JOC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
auto* stream = new (std::nothrow) joc_stream();
|
||||
if (stream == nullptr) {
|
||||
return JOC_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
const joc::Status status = stream->instance.create(to_config(*config));
|
||||
if (!status.ok()) {
|
||||
delete stream;
|
||||
return finish_stream(status);
|
||||
}
|
||||
*out = stream;
|
||||
return JOC_OK;
|
||||
}
|
||||
|
||||
joc_error JOC_CALL joc_stream_push(joc_stream* stream, const joc_stream_buffer* input,
|
||||
std::uint32_t* consumed_samples, std::uint32_t* consumed_bytes) {
|
||||
if (stream == nullptr || input == nullptr ||
|
||||
input->struct_size != sizeof(joc_stream_buffer)) {
|
||||
return JOC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
const joc::Status status = [&] {
|
||||
if (input->kind == JOC_STREAM_IN_EAC3) {
|
||||
std::size_t consumed = 0;
|
||||
const joc::Status pushed =
|
||||
stream->instance.push_eac3(input->bytes, input->byte_count, &consumed);
|
||||
if (consumed_bytes != nullptr) {
|
||||
*consumed_bytes = static_cast<std::uint32_t>(consumed);
|
||||
}
|
||||
return pushed;
|
||||
}
|
||||
if (input->kind == JOC_STREAM_IN_PCM_OBJECTS16) {
|
||||
std::size_t consumed = 0;
|
||||
const joc::Status pushed =
|
||||
stream->instance.push_objects16(input->pcm, input->sample_count, &consumed);
|
||||
if (consumed_samples != nullptr) {
|
||||
*consumed_samples = static_cast<std::uint32_t>(consumed);
|
||||
}
|
||||
return pushed;
|
||||
}
|
||||
std::size_t consumed = 0;
|
||||
const joc::Status pushed =
|
||||
stream->instance.push_bed(input->pcm, input->sample_count, &consumed);
|
||||
if (consumed_samples != nullptr) {
|
||||
*consumed_samples = static_cast<std::uint32_t>(consumed);
|
||||
}
|
||||
return pushed; }();
|
||||
return finish_stream(status);
|
||||
}
|
||||
|
||||
joc_error JOC_CALL joc_stream_pull(joc_stream* stream, joc_stream_buffer* output,
|
||||
std::uint32_t* produced_samples) {
|
||||
if (stream == nullptr || output == nullptr || output->out_pcm == nullptr ||
|
||||
output->struct_size != sizeof(joc_stream_buffer)) {
|
||||
return JOC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
std::size_t produced = 0;
|
||||
const joc::Status status =
|
||||
stream->instance.pull(output->out_pcm, output->sample_count, &produced);
|
||||
if (!status.ok()) {
|
||||
return finish_stream(status);
|
||||
}
|
||||
if (produced_samples != nullptr) {
|
||||
*produced_samples = static_cast<std::uint32_t>(produced);
|
||||
}
|
||||
output->sample_count = static_cast<std::uint32_t>(produced);
|
||||
return JOC_OK;
|
||||
}
|
||||
|
||||
joc_error JOC_CALL joc_stream_flush(joc_stream* stream) {
|
||||
if (stream == nullptr) {
|
||||
return JOC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
return finish_stream(stream->instance.flush());
|
||||
}
|
||||
|
||||
joc_error JOC_CALL joc_stream_reset(joc_stream* stream) {
|
||||
if (stream == nullptr) {
|
||||
return JOC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
return finish_stream(stream->instance.reset());
|
||||
}
|
||||
|
||||
joc_error JOC_CALL joc_stream_status(const joc_stream* stream, joc_stream_status_info* out) {
|
||||
if (stream == nullptr || out == nullptr ||
|
||||
out->struct_size != sizeof(joc_stream_status_info)) {
|
||||
return JOC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
const joc::stream::Info& info = stream->instance.info();
|
||||
out->frames_in = info.frames_in;
|
||||
out->frames_out = info.frames_out;
|
||||
out->samples_in = info.samples_in;
|
||||
out->samples_out = info.samples_out;
|
||||
out->bytes_in = info.bytes_in;
|
||||
out->buffered_samples = stream->instance.buffered_samples();
|
||||
out->oamd_payloads = info.oamd_payloads;
|
||||
out->oamd_transitions = info.oamd_transitions;
|
||||
out->output_channels = info.output_channels;
|
||||
out->ended = info.ended;
|
||||
return JOC_OK;
|
||||
}
|
||||
|
||||
joc_error JOC_CALL joc_stream_destroy(joc_stream* stream) {
|
||||
delete stream;
|
||||
return JOC_OK;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,120 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "foundation/status.h"
|
||||
#include "joc_core.h"
|
||||
#include "task/task.h"
|
||||
|
||||
// task and whichever frontend wants to stop it (plan 29.1/29.2).
|
||||
struct joc_cancel_token {
|
||||
std::atomic<std::uint32_t> requested{0u};
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
thread_local std::string g_task_detail;
|
||||
|
||||
joc_error finish_task(const joc::Status& status) {
|
||||
if (status.ok()) {
|
||||
g_task_detail.clear();
|
||||
return JOC_OK;
|
||||
}
|
||||
g_task_detail.assign(status.stage());
|
||||
g_task_detail.append(": ");
|
||||
g_task_detail.append(status.message());
|
||||
return status.code();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
joc_cancel_token* JOC_CALL joc_cancel_token_create(void) {
|
||||
return new (std::nothrow) joc_cancel_token();
|
||||
}
|
||||
|
||||
void JOC_CALL joc_cancel_token_request(joc_cancel_token* token) {
|
||||
if (token != nullptr) {
|
||||
token->requested.store(1u, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
std::int32_t JOC_CALL joc_cancel_token_is_requested(const joc_cancel_token* token) {
|
||||
return (token != nullptr && token->requested.load(std::memory_order_relaxed) != 0u) ? 1 : 0;
|
||||
}
|
||||
|
||||
void JOC_CALL joc_cancel_token_destroy(joc_cancel_token* token) { delete token; }
|
||||
|
||||
joc_error JOC_CALL joc_task_validate(const joc_task_config* config,
|
||||
joc_validation_issue* issues, std::uint32_t capacity,
|
||||
std::uint32_t* count) {
|
||||
if (config == nullptr) {
|
||||
return JOC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (config->struct_size != sizeof(joc_task_config)) {
|
||||
return JOC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
std::vector<joc_validation_issue> found;
|
||||
std::uint32_t errors = 0;
|
||||
const joc::Status status = joc::task::validate(*config, &found, &errors);
|
||||
if (count != nullptr) {
|
||||
*count = static_cast<std::uint32_t>(found.size());
|
||||
}
|
||||
if (issues != nullptr) {
|
||||
for (std::uint32_t i = 0; i < capacity && i < found.size(); ++i) {
|
||||
issues[i] = found[i];
|
||||
}
|
||||
}
|
||||
return status.ok() ? JOC_OK : JOC_ERR_INVALID_CONFIG;
|
||||
}
|
||||
|
||||
joc_error JOC_CALL joc_task_execute(const joc_task_config* config, const joc_event_sink* sink,
|
||||
joc_task_result* out) {
|
||||
if (config == nullptr || config->struct_size != sizeof(joc_task_config)) {
|
||||
return JOC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (out != nullptr) {
|
||||
std::memset(out, 0, sizeof(*out));
|
||||
out->struct_size = sizeof(joc_task_result);
|
||||
out->struct_version = JOC_TASK_RESULT_VERSION;
|
||||
}
|
||||
const joc::Status status = joc::task::run(*config, sink, out);
|
||||
if (!status.ok() && out != nullptr && out->status == 0u) {
|
||||
out->status = JOC_TASK_FAILED;
|
||||
out->error_code = static_cast<std::uint32_t>(status.code());
|
||||
std::snprintf(out->error_stage, sizeof(out->error_stage), "%s", status.stage().c_str());
|
||||
std::snprintf(out->error_message, sizeof(out->error_message), "%s",
|
||||
status.message().c_str());
|
||||
}
|
||||
g_task_detail = status.ok() ? std::string() : status.message();
|
||||
return status.code();
|
||||
}
|
||||
|
||||
joc_error JOC_CALL joc_task_result_to_json(const joc_task_result* result, char* buffer,
|
||||
std::size_t capacity, std::size_t* needed) {
|
||||
if (result == nullptr) {
|
||||
return JOC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
std::string json;
|
||||
const joc::Status status = joc::task::result_to_json(*result, &json);
|
||||
if (!status.ok()) {
|
||||
return status.code();
|
||||
}
|
||||
if (needed != nullptr) {
|
||||
*needed = json.size() + 1u;
|
||||
}
|
||||
if (buffer == nullptr) {
|
||||
return JOC_OK;
|
||||
}
|
||||
if (capacity < json.size() + 1u) {
|
||||
return JOC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
std::memcpy(buffer, json.c_str(), json.size() + 1u);
|
||||
return JOC_OK;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,664 @@
|
||||
#define EJOC_BUILD_DLL
|
||||
#include "eac3joc_core.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
#include <vector>
|
||||
|
||||
namespace ejoc::binaural {
|
||||
|
||||
struct Complex {
|
||||
double re;
|
||||
double im;
|
||||
};
|
||||
|
||||
inline Complex add(Complex a, Complex b) noexcept {
|
||||
return {a.re + b.re, a.im + b.im};
|
||||
}
|
||||
|
||||
inline Complex mul(Complex a, Complex b) noexcept {
|
||||
return {a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re};
|
||||
}
|
||||
|
||||
inline Complex scale(Complex value, double gain) noexcept {
|
||||
return {value.re * gain, value.im * gain};
|
||||
}
|
||||
|
||||
constexpr double kPi = 3.141592653589793238462643383279502884;
|
||||
constexpr int kChannels = EJOC_BINAURAL_INPUT_CHANNELS;
|
||||
constexpr int kEars = EJOC_BINAURAL_OUTPUT_CHANNELS;
|
||||
constexpr int kBlock = EJOC_BINAURAL_BLOCK_SAMPLES;
|
||||
constexpr int kSlots = kBlock / 64;
|
||||
constexpr int kQmf = EJOC_BINAURAL_QMF_BANDS;
|
||||
constexpr int kHybrid = EJOC_BINAURAL_HYBRID_BANDS;
|
||||
constexpr int kRank = 4;
|
||||
|
||||
class Renderer final {
|
||||
public:
|
||||
Renderer() noexcept {
|
||||
initialize_fft();
|
||||
reset();
|
||||
}
|
||||
|
||||
int configure_kernels(
|
||||
const double* qmf_analysis,
|
||||
const double* hybrid_low,
|
||||
const int16_t* hybrid_indices,
|
||||
const double* hybrid_values,
|
||||
uint32_t hybrid_count,
|
||||
const double* qmf_basis,
|
||||
const double* qmf_taps) noexcept {
|
||||
if (!qmf_analysis || !hybrid_low || !hybrid_indices || !hybrid_values ||
|
||||
!qmf_basis || !qmf_taps || hybrid_count == 0) {
|
||||
return fail("invalid binaural kernel configuration");
|
||||
}
|
||||
std::memcpy(qmf_analysis_.data(), qmf_analysis,
|
||||
qmf_analysis_.size() * sizeof(double));
|
||||
hybrid_low_.assign(hybrid_low, hybrid_low + 3 * 2 * 13 * 16 * 2);
|
||||
hybrid_indices_.assign(hybrid_indices, hybrid_indices + hybrid_count * 4);
|
||||
hybrid_values_.assign(hybrid_values, hybrid_values + hybrid_count);
|
||||
std::memcpy(qmf_basis_.data(), qmf_basis,
|
||||
qmf_basis_.size() * sizeof(double));
|
||||
std::memcpy(qmf_taps_.data(), qmf_taps,
|
||||
qmf_taps_.size() * sizeof(double));
|
||||
kernels_ready_ = true;
|
||||
reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int configure_room(
|
||||
uint32_t bands,
|
||||
uint32_t allpass_count,
|
||||
const uint32_t* allpass_delays,
|
||||
const double* allpass_gains,
|
||||
const uint32_t* fdn_delays,
|
||||
const double* fdn_matrix,
|
||||
uint32_t output_tap_delay,
|
||||
const double* feedback_complex,
|
||||
const double* output_taps,
|
||||
const double* output_complex,
|
||||
uint32_t extra_count,
|
||||
const uint32_t* extra_delays,
|
||||
const double* extra_fields_complex,
|
||||
const double* extra_matrices) noexcept {
|
||||
if (bands != 64 || !fdn_delays || !fdn_matrix || !feedback_complex ||
|
||||
!output_taps || !output_complex ||
|
||||
(allpass_count && (!allpass_delays || !allpass_gains)) ||
|
||||
(extra_count && (!extra_delays || !extra_fields_complex || !extra_matrices))) {
|
||||
return fail("invalid binaural room configuration");
|
||||
}
|
||||
room_bands_ = bands;
|
||||
if (allpass_count) {
|
||||
allpass_delays_.assign(allpass_delays, allpass_delays + allpass_count);
|
||||
allpass_gains_.assign(allpass_gains, allpass_gains + allpass_count);
|
||||
} else {
|
||||
allpass_delays_.clear();
|
||||
allpass_gains_.clear();
|
||||
}
|
||||
allpass_offsets_.resize(allpass_count);
|
||||
allpass_positions_.assign(allpass_count, 0);
|
||||
size_t allpass_size = 0;
|
||||
for (uint32_t index = 0; index < allpass_count; ++index) {
|
||||
if (allpass_delays_[index] == 0) {
|
||||
return fail("binaural allpass delay must be positive");
|
||||
}
|
||||
allpass_offsets_[index] = allpass_size;
|
||||
allpass_size += static_cast<size_t>(allpass_delays_[index]) * bands;
|
||||
}
|
||||
allpass_memory_.assign(allpass_size, {});
|
||||
|
||||
room_capacity_ = 0;
|
||||
for (int branch = 0; branch < 4; ++branch) {
|
||||
fdn_delays_[branch] = fdn_delays[branch];
|
||||
room_capacity_ = std::max(room_capacity_, fdn_delays_[branch]);
|
||||
}
|
||||
if (room_capacity_ == 0) {
|
||||
return fail("binaural room delay must be positive");
|
||||
}
|
||||
std::copy(fdn_matrix, fdn_matrix + 16, fdn_matrix_.begin());
|
||||
output_tap_delay_ = output_tap_delay;
|
||||
for (int band = 0; band < 64; ++band) {
|
||||
for (int branch = 0; branch < 4; ++branch) {
|
||||
const size_t complex_index = (static_cast<size_t>(band) * 4 + branch) * 2;
|
||||
feedback_[band][branch] = {
|
||||
feedback_complex[complex_index], feedback_complex[complex_index + 1]};
|
||||
output_taps_[band][branch] = output_taps[band * 4 + branch];
|
||||
for (int ear = 0; ear < 2; ++ear) {
|
||||
const size_t output_index =
|
||||
((static_cast<size_t>(ear) * 64 + band) * 4 + branch) * 2;
|
||||
output_matrix_[ear][band][branch] = {
|
||||
output_complex[output_index], output_complex[output_index + 1]};
|
||||
}
|
||||
}
|
||||
}
|
||||
room_memory_.assign(static_cast<size_t>(room_capacity_) * 64 * 4, {});
|
||||
if (extra_count) {
|
||||
extra_delays_.assign(extra_delays, extra_delays + extra_count);
|
||||
} else {
|
||||
extra_delays_.clear();
|
||||
}
|
||||
extra_fields_.resize(static_cast<size_t>(extra_count) * 64);
|
||||
extra_matrices_.resize(static_cast<size_t>(extra_count) * 16);
|
||||
for (uint32_t extra = 0; extra < extra_count; ++extra) {
|
||||
for (int band = 0; band < 64; ++band) {
|
||||
const size_t source = (static_cast<size_t>(extra) * 64 + band) * 2;
|
||||
extra_fields_[static_cast<size_t>(extra) * 64 + band] = {
|
||||
extra_fields_complex[source], extra_fields_complex[source + 1]};
|
||||
}
|
||||
std::copy(extra_matrices + static_cast<size_t>(extra) * 16,
|
||||
extra_matrices + static_cast<size_t>(extra + 1) * 16,
|
||||
extra_matrices_.begin() + static_cast<size_t>(extra) * 16);
|
||||
}
|
||||
room_ready_ = true;
|
||||
reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int reset() noexcept {
|
||||
qmf_history_.fill(0.0);
|
||||
hybrid_low_history_.fill({});
|
||||
hybrid_high_history_.fill({});
|
||||
synthesis_history_.fill(0.0);
|
||||
std::fill(allpass_memory_.begin(), allpass_memory_.end(), Complex{});
|
||||
std::fill(allpass_positions_.begin(), allpass_positions_.end(), 0u);
|
||||
std::fill(room_memory_.begin(), room_memory_.end(), Complex{});
|
||||
room_position_ = 0;
|
||||
error_[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* error() const noexcept {
|
||||
return error_[0] ? error_ : "";
|
||||
}
|
||||
|
||||
int process(
|
||||
const double* input,
|
||||
const double* gains,
|
||||
const double* room_sends,
|
||||
double output_gain,
|
||||
double* output) noexcept {
|
||||
if (!kernels_ready_ || !room_ready_) {
|
||||
return fail("binaural renderer is not configured");
|
||||
}
|
||||
if (!input || !gains || !room_sends || !output || !std::isfinite(output_gain)) {
|
||||
return fail("invalid binaural process arguments");
|
||||
}
|
||||
for (int slot = 0; slot < kSlots; ++slot) {
|
||||
std::array<Complex, kChannels * kQmf> qmf{};
|
||||
std::array<Complex, kChannels * kHybrid> hybrid{};
|
||||
analyze_qmf(input + static_cast<size_t>(slot) * 64 * kChannels, qmf);
|
||||
analyze_hybrid(qmf, hybrid);
|
||||
|
||||
std::array<Complex, kEars * kHybrid> rendered{};
|
||||
std::array<Complex, kHybrid> room_input{};
|
||||
for (int source = kChannels - 1; source >= 0; --source) {
|
||||
for (int band = 0; band < kHybrid; ++band) {
|
||||
const Complex value = hybrid[source * kHybrid + band];
|
||||
room_input[band] = add(room_input[band], scale(value, room_sends[source]));
|
||||
for (int ear = 0; ear < kEars; ++ear) {
|
||||
const size_t gain_index =
|
||||
(((static_cast<size_t>(source) * kEars + ear) * kHybrid + band) * 2);
|
||||
const Complex gain{gains[gain_index], gains[gain_index + 1]};
|
||||
rendered[ear * kHybrid + band] = add(
|
||||
rendered[ear * kHybrid + band], mul(value, gain));
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto room = process_room(room_input);
|
||||
for (size_t index = 0; index < rendered.size(); ++index) {
|
||||
rendered[index] = add(rendered[index], room[index]);
|
||||
}
|
||||
|
||||
std::array<Complex, kEars * kQmf> qmf_output{};
|
||||
synthesize_hybrid(rendered, qmf_output);
|
||||
for (int ear = 0; ear < kEars; ++ear) {
|
||||
std::array<double, 64> samples{};
|
||||
synthesize_qmf(qmf_output.data() + ear * kQmf, ear, samples);
|
||||
for (int sample = 0; sample < 64; ++sample) {
|
||||
output[(static_cast<size_t>(slot) * 64 + sample) * 2 + ear] =
|
||||
samples[sample] * output_gain;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
int fail(const char* message) noexcept {
|
||||
std::snprintf(error_, sizeof(error_), "%s", message);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void initialize_fft() noexcept {
|
||||
for (int index = 0; index < 128; ++index) {
|
||||
int value = index;
|
||||
int reversed = 0;
|
||||
for (int bit = 0; bit < 7; ++bit) {
|
||||
reversed = (reversed << 1) | (value & 1);
|
||||
value >>= 1;
|
||||
}
|
||||
bit_reverse_[index] = static_cast<uint8_t>(reversed);
|
||||
}
|
||||
for (int phase = 0; phase < 64; ++phase) {
|
||||
const double angle = -kPi * static_cast<double>(phase) / 128.0;
|
||||
premod_[phase] = {std::cos(angle), std::sin(angle)};
|
||||
const double post_angle =
|
||||
-3.0 * (static_cast<double>(phase) + 0.5) * kPi / 128.0;
|
||||
post_[phase] = {std::cos(post_angle), std::sin(post_angle)};
|
||||
even_post_[phase] = {0.0, (phase & 1) ? -1.0 : 1.0};
|
||||
}
|
||||
}
|
||||
|
||||
void fft128(std::array<Complex, 128>& values) const noexcept {
|
||||
for (int index = 0; index < 128; ++index) {
|
||||
const int reversed = bit_reverse_[index];
|
||||
if (reversed > index) {
|
||||
std::swap(values[index], values[reversed]);
|
||||
}
|
||||
}
|
||||
for (int length = 2; length <= 128; length <<= 1) {
|
||||
const double angle = -2.0 * kPi / static_cast<double>(length);
|
||||
const Complex step{std::cos(angle), std::sin(angle)};
|
||||
for (int start = 0; start < 128; start += length) {
|
||||
Complex rotation{1.0, 0.0};
|
||||
for (int offset = 0; offset < length / 2; ++offset) {
|
||||
const Complex even = values[start + offset];
|
||||
const Complex odd = mul(values[start + offset + length / 2], rotation);
|
||||
values[start + offset] = {even.re + odd.re, even.im + odd.im};
|
||||
values[start + offset + length / 2] = {
|
||||
even.re - odd.re, even.im - odd.im};
|
||||
rotation = mul(rotation, step);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void qmf_transform(const std::array<double, 64>& source,
|
||||
std::array<Complex, 64>& target) const noexcept {
|
||||
std::array<Complex, 128> work{};
|
||||
for (int phase = 0; phase < 64; ++phase) {
|
||||
work[phase] = scale(premod_[phase], source[phase]);
|
||||
}
|
||||
fft128(work);
|
||||
for (int band = 0; band < 64; ++band) {
|
||||
target[band] = mul(work[band], post_[band]);
|
||||
}
|
||||
}
|
||||
|
||||
void analyze_qmf(const double* input,
|
||||
std::array<Complex, kChannels * kQmf>& output) noexcept {
|
||||
for (int channel = 0; channel < kChannels; ++channel) {
|
||||
for (int lag = 9; lag > 0; --lag) {
|
||||
for (int phase = 0; phase < 64; ++phase) {
|
||||
qmf_history_[qmf_history_index(lag, channel, phase)] =
|
||||
qmf_history_[qmf_history_index(lag - 1, channel, phase)];
|
||||
}
|
||||
}
|
||||
for (int phase = 0; phase < 64; ++phase) {
|
||||
qmf_history_[qmf_history_index(0, channel, phase)] =
|
||||
input[phase * kChannels + channel];
|
||||
}
|
||||
std::array<double, 64> even{};
|
||||
std::array<double, 64> odd{};
|
||||
for (int phase = 0; phase < 64; ++phase) {
|
||||
for (int lag = 0; lag < 10; ++lag) {
|
||||
const double value =
|
||||
qmf_history_[qmf_history_index(lag, channel, phase)] *
|
||||
qmf_analysis_[phase * 10 + lag];
|
||||
(lag & 1 ? odd[phase] : even[phase]) += value;
|
||||
}
|
||||
}
|
||||
std::array<Complex, 64> even_fft{};
|
||||
std::array<Complex, 64> odd_fft{};
|
||||
qmf_transform(even, even_fft);
|
||||
qmf_transform(odd, odd_fft);
|
||||
for (int band = 0; band < 64; ++band) {
|
||||
output[channel * 64 + band] = add(
|
||||
odd_fft[band], mul(even_fft[band], even_post_[band]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void analyze_hybrid(
|
||||
const std::array<Complex, kChannels * kQmf>& qmf,
|
||||
std::array<Complex, kChannels * kHybrid>& output) noexcept {
|
||||
for (int channel = 0; channel < kChannels; ++channel) {
|
||||
for (int lag = 12; lag > 0; --lag) {
|
||||
for (int band = 0; band < 3; ++band) {
|
||||
hybrid_low_history_[hybrid_low_history_index(lag, channel, band)] =
|
||||
hybrid_low_history_[hybrid_low_history_index(lag - 1, channel, band)];
|
||||
}
|
||||
}
|
||||
for (int band = 0; band < 3; ++band) {
|
||||
hybrid_low_history_[hybrid_low_history_index(0, channel, band)] =
|
||||
qmf[channel * 64 + band];
|
||||
}
|
||||
for (int output_band = 0; output_band < 16; ++output_band) {
|
||||
Complex value{};
|
||||
for (int lag = 0; lag < 13; ++lag) {
|
||||
for (int input_band = 0; input_band < 3; ++input_band) {
|
||||
const Complex source = hybrid_low_history_[
|
||||
hybrid_low_history_index(lag, channel, input_band)];
|
||||
const double components[2]{source.re, source.im};
|
||||
for (int input_component = 0; input_component < 2; ++input_component) {
|
||||
value.re += components[input_component] * hybrid_low_[
|
||||
hybrid_low_kernel_index(input_band, input_component, lag,
|
||||
output_band, 0)];
|
||||
value.im += components[input_component] * hybrid_low_[
|
||||
hybrid_low_kernel_index(input_band, input_component, lag,
|
||||
output_band, 1)];
|
||||
}
|
||||
}
|
||||
}
|
||||
output[channel * kHybrid + output_band] = value;
|
||||
}
|
||||
for (int band = 0; band < 61; ++band) {
|
||||
output[channel * kHybrid + 16 + band] =
|
||||
hybrid_high_history_[hybrid_high_history_index(0, channel, band)];
|
||||
for (int delay = 0; delay < 5; ++delay) {
|
||||
hybrid_high_history_[hybrid_high_history_index(delay, channel, band)] =
|
||||
hybrid_high_history_[hybrid_high_history_index(delay + 1, channel, band)];
|
||||
}
|
||||
hybrid_high_history_[hybrid_high_history_index(5, channel, band)] =
|
||||
qmf[channel * 64 + 3 + band];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::array<Complex, kEars * kHybrid> process_room(
|
||||
const std::array<Complex, kHybrid>& input) noexcept {
|
||||
std::array<Complex, 64> filtered{};
|
||||
for (int band = 0; band < 64; ++band) {
|
||||
filtered[band] = scale(input[band], 0.70710677);
|
||||
}
|
||||
for (size_t stage = 0; stage < allpass_delays_.size(); ++stage) {
|
||||
const uint32_t position = allpass_positions_[stage];
|
||||
const double gain = allpass_gains_[stage];
|
||||
for (int band = 0; band < 64; ++band) {
|
||||
Complex& memory = allpass_memory_[
|
||||
allpass_offsets_[stage] + static_cast<size_t>(position) * 64 + band];
|
||||
const Complex residual = add(filtered[band], scale(memory, -gain));
|
||||
filtered[band] = add(scale(residual, gain), memory);
|
||||
memory = residual;
|
||||
}
|
||||
allpass_positions_[stage] = (position + 1) % allpass_delays_[stage];
|
||||
}
|
||||
|
||||
std::array<Complex, 64 * 4> branches{};
|
||||
std::array<Complex, 64 * 4> taps{};
|
||||
for (int band = 0; band < 64; ++band) {
|
||||
for (int branch = 0; branch < 4; ++branch) {
|
||||
Complex value = filtered[band];
|
||||
for (int source = 0; source < 4; ++source) {
|
||||
const uint32_t position =
|
||||
(room_position_ + room_capacity_ - fdn_delays_[source]) % room_capacity_;
|
||||
value = add(value, scale(room_memory_[
|
||||
room_memory_index(position, band, source)],
|
||||
fdn_matrix_[branch * 4 + source]));
|
||||
}
|
||||
branches[band * 4 + branch] = value;
|
||||
const uint32_t tap_position =
|
||||
(room_position_ + room_capacity_ -
|
||||
(output_tap_delay_ % room_capacity_)) % room_capacity_;
|
||||
taps[band * 4 + branch] =
|
||||
room_memory_[room_memory_index(tap_position, band, branch)];
|
||||
}
|
||||
}
|
||||
for (int band = 0; band < 64; ++band) {
|
||||
for (int branch = 0; branch < 4; ++branch) {
|
||||
room_memory_[room_memory_index(room_position_, band, branch)] =
|
||||
mul(branches[band * 4 + branch], feedback_[band][branch]);
|
||||
}
|
||||
}
|
||||
room_position_ = (room_position_ + 1) % room_capacity_;
|
||||
|
||||
std::array<Complex, 64 * 4> extra{};
|
||||
for (size_t index = 0; index < extra_delays_.size(); ++index) {
|
||||
const uint32_t position =
|
||||
(room_position_ + room_capacity_ -
|
||||
((extra_delays_[index] + 1) % room_capacity_)) % room_capacity_;
|
||||
for (int band = 0; band < 64; ++band) {
|
||||
for (int target = 0; target < 4; ++target) {
|
||||
Complex mixed{};
|
||||
for (int source = 0; source < 4; ++source) {
|
||||
mixed = add(mixed, scale(room_memory_[
|
||||
room_memory_index(position, band, source)],
|
||||
extra_matrices_[index * 16 + target * 4 + source]));
|
||||
}
|
||||
extra[band * 4 + target] = add(
|
||||
extra[band * 4 + target],
|
||||
mul(mixed, extra_fields_[index * 64 + band]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::array<Complex, kEars * kHybrid> output{};
|
||||
for (int ear = 0; ear < 2; ++ear) {
|
||||
for (int band = 0; band < 64; ++band) {
|
||||
Complex value{};
|
||||
for (int branch = 0; branch < 4; ++branch) {
|
||||
const Complex signal = add(
|
||||
scale(taps[band * 4 + branch], output_taps_[band][branch]),
|
||||
extra[band * 4 + branch]);
|
||||
value = add(value, mul(
|
||||
signal, output_matrix_[ear][band][branch]));
|
||||
}
|
||||
output[ear * kHybrid + band] = value;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
void synthesize_hybrid(
|
||||
const std::array<Complex, kEars * kHybrid>& input,
|
||||
std::array<Complex, kEars * kQmf>& output) const noexcept {
|
||||
for (size_t mapping = 0; mapping < hybrid_values_.size(); ++mapping) {
|
||||
const int16_t* index = hybrid_indices_.data() + mapping * 4;
|
||||
const int input_band = index[0];
|
||||
const int input_component = index[1];
|
||||
const int output_band = index[2];
|
||||
const int output_component = index[3];
|
||||
const double gain = hybrid_values_[mapping];
|
||||
for (int ear = 0; ear < 2; ++ear) {
|
||||
const Complex source = input[ear * kHybrid + input_band];
|
||||
Complex& target = output[ear * kQmf + output_band];
|
||||
const double component = input_component == 0 ? source.re : source.im;
|
||||
(output_component == 0 ? target.re : target.im) += component * gain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void synthesize_qmf(const Complex* input, int ear,
|
||||
std::array<double, 64>& output) noexcept {
|
||||
std::array<double, 64 * kRank> features{};
|
||||
std::array<double, 128> flat{};
|
||||
for (int band = 0; band < 64; ++band) {
|
||||
flat[band * 2] = input[band].re;
|
||||
flat[band * 2 + 1] = input[band].im;
|
||||
}
|
||||
for (int phase = 0; phase < 64; ++phase) {
|
||||
for (int rank = 0; rank < kRank; ++rank) {
|
||||
double value = 0.0;
|
||||
const size_t base = (static_cast<size_t>(phase) * kRank + rank) * 128;
|
||||
for (int component = 0; component < 128; ++component) {
|
||||
value += flat[component] * qmf_basis_[base + component];
|
||||
}
|
||||
features[phase * kRank + rank] = value;
|
||||
}
|
||||
}
|
||||
for (int phase = 0; phase < 64; ++phase) {
|
||||
double value = 0.0;
|
||||
for (int lag = 0; lag < 10; ++lag) {
|
||||
for (int rank = 0; rank < kRank; ++rank) {
|
||||
const double feature = lag == 0
|
||||
? features[phase * kRank + rank]
|
||||
: synthesis_history_[synthesis_history_index(
|
||||
ear, lag - 1, phase, rank)];
|
||||
value += feature * qmf_taps_[
|
||||
((static_cast<size_t>(phase) * 10 + lag) * kRank + rank)];
|
||||
}
|
||||
}
|
||||
output[phase] = value;
|
||||
}
|
||||
for (int lag = 8; lag > 0; --lag) {
|
||||
for (int phase = 0; phase < 64; ++phase) {
|
||||
for (int rank = 0; rank < kRank; ++rank) {
|
||||
synthesis_history_[synthesis_history_index(ear, lag, phase, rank)] =
|
||||
synthesis_history_[synthesis_history_index(
|
||||
ear, lag - 1, phase, rank)];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int phase = 0; phase < 64; ++phase) {
|
||||
for (int rank = 0; rank < kRank; ++rank) {
|
||||
synthesis_history_[synthesis_history_index(ear, 0, phase, rank)] =
|
||||
features[phase * kRank + rank];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static size_t qmf_history_index(int lag, int channel, int phase) noexcept {
|
||||
return (static_cast<size_t>(lag) * kChannels + channel) * 64 + phase;
|
||||
}
|
||||
|
||||
static size_t hybrid_low_history_index(int lag, int channel, int band) noexcept {
|
||||
return (static_cast<size_t>(lag) * kChannels + channel) * 3 + band;
|
||||
}
|
||||
|
||||
static size_t hybrid_high_history_index(int delay, int channel, int band) noexcept {
|
||||
return (static_cast<size_t>(delay) * kChannels + channel) * 61 + band;
|
||||
}
|
||||
|
||||
static size_t hybrid_low_kernel_index(
|
||||
int input_band, int input_component, int lag,
|
||||
int output_band, int output_component) noexcept {
|
||||
return (((static_cast<size_t>(input_band) * 2 + input_component) * 13 + lag) *
|
||||
16 + output_band) * 2 + output_component;
|
||||
}
|
||||
|
||||
size_t room_memory_index(uint32_t position, int band, int branch) const noexcept {
|
||||
return (static_cast<size_t>(position) * 64 + band) * 4 + branch;
|
||||
}
|
||||
|
||||
static size_t synthesis_history_index(
|
||||
int ear, int lag, int phase, int rank) noexcept {
|
||||
return (((static_cast<size_t>(ear) * 9 + lag) * 64 + phase) * kRank + rank);
|
||||
}
|
||||
|
||||
bool kernels_ready_ = false;
|
||||
bool room_ready_ = false;
|
||||
std::array<double, 64 * 10> qmf_analysis_{};
|
||||
std::vector<double> hybrid_low_;
|
||||
std::vector<int16_t> hybrid_indices_;
|
||||
std::vector<double> hybrid_values_;
|
||||
std::array<double, 64 * kRank * 128> qmf_basis_{};
|
||||
std::array<double, 64 * 10 * kRank> qmf_taps_{};
|
||||
|
||||
std::array<double, 10 * kChannels * 64> qmf_history_{};
|
||||
std::array<Complex, 13 * kChannels * 3> hybrid_low_history_{};
|
||||
std::array<Complex, 6 * kChannels * 61> hybrid_high_history_{};
|
||||
std::array<double, kEars * 9 * 64 * kRank> synthesis_history_{};
|
||||
|
||||
uint32_t room_bands_ = 0;
|
||||
std::vector<uint32_t> allpass_delays_;
|
||||
std::vector<double> allpass_gains_;
|
||||
std::vector<size_t> allpass_offsets_;
|
||||
std::vector<uint32_t> allpass_positions_;
|
||||
std::vector<Complex> allpass_memory_;
|
||||
std::array<uint32_t, 4> fdn_delays_{};
|
||||
std::array<double, 16> fdn_matrix_{};
|
||||
uint32_t room_capacity_ = 0;
|
||||
uint32_t output_tap_delay_ = 0;
|
||||
std::array<std::array<Complex, 4>, 64> feedback_{};
|
||||
std::array<std::array<double, 4>, 64> output_taps_{};
|
||||
std::array<std::array<std::array<Complex, 4>, 64>, 2> output_matrix_{};
|
||||
std::vector<Complex> room_memory_;
|
||||
uint32_t room_position_ = 0;
|
||||
std::vector<uint32_t> extra_delays_;
|
||||
std::vector<Complex> extra_fields_;
|
||||
std::vector<double> extra_matrices_;
|
||||
|
||||
std::array<uint8_t, 128> bit_reverse_{};
|
||||
std::array<Complex, 64> premod_{};
|
||||
std::array<Complex, 64> post_{};
|
||||
std::array<Complex, 64> even_post_{};
|
||||
char error_[256]{};
|
||||
};
|
||||
|
||||
} // namespace ejoc::binaural
|
||||
|
||||
extern "C" {
|
||||
|
||||
ejoc_binaural_renderer_handle EJOC_CALL ejoc_binaural_renderer_create(void) {
|
||||
return new (std::nothrow) ejoc::binaural::Renderer();
|
||||
}
|
||||
|
||||
void EJOC_CALL ejoc_binaural_renderer_destroy(ejoc_binaural_renderer_handle handle) {
|
||||
delete static_cast<ejoc::binaural::Renderer*>(handle);
|
||||
}
|
||||
|
||||
int EJOC_CALL ejoc_binaural_renderer_reset(ejoc_binaural_renderer_handle handle) {
|
||||
return handle ? static_cast<ejoc::binaural::Renderer*>(handle)->reset() : -1;
|
||||
}
|
||||
|
||||
const char* EJOC_CALL ejoc_binaural_renderer_last_error(
|
||||
ejoc_binaural_renderer_handle handle) {
|
||||
return handle ? static_cast<ejoc::binaural::Renderer*>(handle)->error()
|
||||
: "null binaural renderer handle";
|
||||
}
|
||||
|
||||
int EJOC_CALL ejoc_binaural_renderer_configure_kernels(
|
||||
ejoc_binaural_renderer_handle handle,
|
||||
const double* qmf_analysis,
|
||||
const double* hybrid_low,
|
||||
const int16_t* hybrid_indices,
|
||||
const double* hybrid_values,
|
||||
uint32_t hybrid_count,
|
||||
const double* qmf_basis,
|
||||
const double* qmf_taps) {
|
||||
return handle ? static_cast<ejoc::binaural::Renderer*>(handle)->configure_kernels(
|
||||
qmf_analysis, hybrid_low, hybrid_indices, hybrid_values,
|
||||
hybrid_count, qmf_basis, qmf_taps) : -1;
|
||||
}
|
||||
|
||||
int EJOC_CALL ejoc_binaural_renderer_configure_room(
|
||||
ejoc_binaural_renderer_handle handle,
|
||||
uint32_t bands,
|
||||
uint32_t allpass_count,
|
||||
const uint32_t* allpass_delays,
|
||||
const double* allpass_gains,
|
||||
const uint32_t* fdn_delays,
|
||||
const double* fdn_matrix,
|
||||
uint32_t output_tap_delay,
|
||||
const double* feedback_complex,
|
||||
const double* output_taps,
|
||||
const double* output_complex,
|
||||
uint32_t extra_count,
|
||||
const uint32_t* extra_delays,
|
||||
const double* extra_fields_complex,
|
||||
const double* extra_matrices) {
|
||||
return handle ? static_cast<ejoc::binaural::Renderer*>(handle)->configure_room(
|
||||
bands, allpass_count, allpass_delays, allpass_gains,
|
||||
fdn_delays, fdn_matrix, output_tap_delay,
|
||||
feedback_complex, output_taps, output_complex,
|
||||
extra_count, extra_delays, extra_fields_complex, extra_matrices) : -1;
|
||||
}
|
||||
|
||||
int EJOC_CALL ejoc_binaural_renderer_process(
|
||||
ejoc_binaural_renderer_handle handle,
|
||||
const double* input16_interleaved,
|
||||
const double* gains_complex,
|
||||
const double* room_sends,
|
||||
double output_gain,
|
||||
double* output_stereo_interleaved) {
|
||||
return handle ? static_cast<ejoc::binaural::Renderer*>(handle)->process(
|
||||
input16_interleaved, gains_complex, room_sends,
|
||||
output_gain, output_stereo_interleaved) : -1;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,251 @@
|
||||
#include "binaural/binaural_runtime.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace joc::binaural {
|
||||
|
||||
namespace {
|
||||
|
||||
double max_delay_bound(const hrtf::Field& field) {
|
||||
double maximum = 0.0;
|
||||
for (const double value : field.delay_bounds) {
|
||||
maximum = std::max(maximum, value);
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool profile_from_name(const char* name, Profile* out) {
|
||||
if (name == nullptr || out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (std::strcmp(name, "near") == 0) { *out = Profile::Near; return true; }
|
||||
if (std::strcmp(name, "mid") == 0) { *out = Profile::Mid; return true; }
|
||||
if (std::strcmp(name, "far") == 0) { *out = Profile::Far; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
SofaBinauralRuntime::~SofaBinauralRuntime() {
|
||||
if (handle_ != nullptr) {
|
||||
ejoc_sofa_binaural_destroy(handle_);
|
||||
handle_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Status SofaBinauralRuntime::open(const hrtf::Field& field, const hrtf::Kernels& kernels,
|
||||
Profile profile, const RoomConstants& room) {
|
||||
if (handle_ != nullptr) {
|
||||
return Status::fail(JOC_ERR_STATE, stage::kRender, "binaural runtime already open");
|
||||
}
|
||||
if (field.coefficients.size() != static_cast<std::size_t>(hrtf::kShTerms * hrtf::kEars *
|
||||
hrtf::kHybridBands * 2) ||
|
||||
field.band_centers_hz.size() != static_cast<std::size_t>(hrtf::kHybridBands)) {
|
||||
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender,
|
||||
"compiled HRTF field has unexpected array sizes");
|
||||
}
|
||||
handle_ = ejoc_sofa_binaural_create();
|
||||
if (handle_ == nullptr) {
|
||||
return Status::fail(JOC_ERR_OUT_OF_MEMORY, stage::kRender,
|
||||
"ejoc_sofa_binaural_create failed");
|
||||
}
|
||||
profile_ = profile;
|
||||
|
||||
if (ejoc_sofa_binaural_configure_kernels(
|
||||
handle_, kernels.qmf_analysis.data(), kernels.hybrid_low.data(),
|
||||
kernels.hybrid_indices.data(), kernels.hybrid_values.data(), kernels.hybrid_count,
|
||||
kernels.qmf_basis.data(), kernels.qmf_taps.data()) != 0) {
|
||||
const char* message = ejoc_sofa_binaural_last_error(handle_);
|
||||
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender,
|
||||
std::string("configure_kernels failed: ") +
|
||||
(message != nullptr ? message : "unknown"));
|
||||
}
|
||||
if (ejoc_sofa_binaural_configure_field(handle_, field.coefficients.data(),
|
||||
field.delay_coefficients.data(),
|
||||
field.delay_bounds.data(), field.band_centers_hz.data(),
|
||||
field.measurement_radius_m) != 0) {
|
||||
const char* message = ejoc_sofa_binaural_last_error(handle_);
|
||||
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender,
|
||||
std::string("configure_field failed: ") +
|
||||
(message != nullptr ? message : "unknown"));
|
||||
}
|
||||
if (ejoc_sofa_binaural_configure_room(
|
||||
handle_, room.dims, room.listener, room.walls, room.speed_of_sound, room.fdn_delays,
|
||||
room.fdn_feedback, room.damping, room.fdn_output_gain, room.allpass_delays,
|
||||
room.allpass_gains, room.enable_early_reflections, room.enable_late_room) != 0) {
|
||||
const char* message = ejoc_sofa_binaural_last_error(handle_);
|
||||
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender,
|
||||
std::string("configure_room failed: ") +
|
||||
(message != nullptr ? message : "unknown"));
|
||||
}
|
||||
|
||||
maximum_hrtf_delay_ =
|
||||
static_cast<std::int64_t>(std::ceil(max_delay_bound(field) - 1e-9));
|
||||
hrtf_history_slots_ = static_cast<std::uint32_t>(std::max<std::int64_t>(1, (maximum_hrtf_delay_ + 63) / 64));
|
||||
staging_.assign(kBlockSamples * kSourceCount, 0.0);
|
||||
block_output_.assign(kBlockSamples * 2u, 0.0);
|
||||
output_.clear();
|
||||
staged_ = 0;
|
||||
input_samples_ = 0;
|
||||
processed_samples_ = 0;
|
||||
blocks_processed_ = 0;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status SofaBinauralRuntime::process_block() {
|
||||
double positions[timeline::kTimelineObjects][3] = {};
|
||||
const Status queried = timeline_.positions_at(static_cast<std::int64_t>(processed_samples_),
|
||||
positions);
|
||||
if (!queried.ok()) {
|
||||
return queried;
|
||||
}
|
||||
// The reference adapter calls set_source without a `fade` argument, so the
|
||||
// backend default (fade enabled) applies - the per-object path crossfade is
|
||||
// part of the reference behaviour, not an optional extra.
|
||||
constexpr std::uint32_t kFade = 1u;
|
||||
if (ejoc_sofa_binaural_set_source(handle_, 0u, kLfePosition,
|
||||
static_cast<std::uint32_t>(profile_), 1.0, 1u, 1u,
|
||||
kFade) != 0) {
|
||||
const char* message = ejoc_sofa_binaural_last_error(handle_);
|
||||
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender,
|
||||
std::string("set_source(LFE) failed: ") +
|
||||
(message != nullptr ? message : "unknown"));
|
||||
}
|
||||
for (std::uint32_t source = 0; source < timeline::kTimelineObjects; ++source) {
|
||||
if (ejoc_sofa_binaural_set_source(handle_, source + 1u, positions[source],
|
||||
static_cast<std::uint32_t>(profile_), 1.0, 1u, 0u,
|
||||
kFade) != 0) {
|
||||
const char* message = ejoc_sofa_binaural_last_error(handle_);
|
||||
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender,
|
||||
std::string("set_source(object ") + std::to_string(source + 1u) +
|
||||
") failed: " + (message != nullptr ? message : "unknown"));
|
||||
}
|
||||
}
|
||||
const int trimmed = ejoc_sofa_binaural_process(handle_, staging_.data(), kBlockSamples, 1.0,
|
||||
block_output_.data());
|
||||
if (trimmed < 0) {
|
||||
const char* message = ejoc_sofa_binaural_last_error(handle_);
|
||||
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender,
|
||||
std::string("sofa process failed: ") +
|
||||
(message != nullptr ? message : "unknown"));
|
||||
}
|
||||
if (trimmed > 0) {
|
||||
output_.insert(output_.end(), block_output_.begin(),
|
||||
block_output_.begin() + static_cast<std::ptrdiff_t>(trimmed) * 2);
|
||||
}
|
||||
staged_ = 0;
|
||||
processed_samples_ += kBlockSamples;
|
||||
++blocks_processed_;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status SofaBinauralRuntime::submit_frame(const float* objects16_planar,
|
||||
const oamd::OamdUpdate* update, std::int64_t frame_index,
|
||||
std::int64_t outer_sample_offset,
|
||||
std::int64_t object_delay_samples) {
|
||||
if (handle_ == nullptr) {
|
||||
return Status::fail(JOC_ERR_STATE, stage::kRender, "binaural runtime is not open");
|
||||
}
|
||||
if (objects16_planar == nullptr) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kRender, "null frame");
|
||||
}
|
||||
// A frame without an ID11 payload submits nothing at all (the reference only
|
||||
if (update != nullptr) {
|
||||
const Status submitted = timeline_.submit_update(
|
||||
*update, frame_index * JOC_FRAME_SAMPLES, outer_sample_offset, object_delay_samples,
|
||||
static_cast<std::int64_t>(input_samples_));
|
||||
if (!submitted.ok()) {
|
||||
return submitted;
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t offset = 0;
|
||||
while (offset < JOC_FRAME_SAMPLES) {
|
||||
const std::size_t room = kBlockSamples - staged_;
|
||||
const std::size_t count = std::min<std::size_t>(room, JOC_FRAME_SAMPLES - offset);
|
||||
for (std::size_t sample = 0; sample < count; ++sample) {
|
||||
double* row = staging_.data() + (staged_ + sample) * kSourceCount;
|
||||
for (std::size_t channel = 0; channel < kSourceCount; ++channel) {
|
||||
row[channel] = static_cast<double>(
|
||||
objects16_planar[channel * JOC_FRAME_SAMPLES + offset + sample]);
|
||||
}
|
||||
}
|
||||
staged_ += count;
|
||||
offset += count;
|
||||
if (staged_ == kBlockSamples) {
|
||||
const Status status = process_block();
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
input_samples_ += JOC_FRAME_SAMPLES;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
std::uint32_t SofaBinauralRuntime::finish_capacity(double tail_seconds) const {
|
||||
std::int64_t requested = tail_samples_;
|
||||
if (tail_seconds >= 0.0) {
|
||||
requested = static_cast<std::int64_t>(std::ceil(tail_seconds * 48000.0 - 1e-9));
|
||||
}
|
||||
const std::int64_t hrtf_bound = static_cast<std::int64_t>(hrtf_history_slots_) * 64;
|
||||
const std::int64_t early_bound = hrtf_bound + 2048 + 256 * 64;
|
||||
std::int64_t drain = std::max(requested, early_bound) + 961;
|
||||
drain = ((drain + 63) / 64) * 64;
|
||||
return static_cast<std::uint32_t>(drain);
|
||||
}
|
||||
|
||||
Status SofaBinauralRuntime::reset() {
|
||||
if (handle_ == nullptr) {
|
||||
return Status::fail(JOC_ERR_STATE, stage::kRender, "binaural runtime is not open");
|
||||
}
|
||||
if (ejoc_sofa_binaural_reset(handle_) != 0) {
|
||||
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender, "sofa reset failed");
|
||||
}
|
||||
timeline_ = timeline::OamdPositionTimeline();
|
||||
output_.clear();
|
||||
staged_ = 0;
|
||||
input_samples_ = 0;
|
||||
processed_samples_ = 0;
|
||||
blocks_processed_ = 0;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status SofaBinauralRuntime::finish(std::uint32_t flush_samples, std::vector<double>* out) {
|
||||
if (handle_ == nullptr) {
|
||||
return Status::fail(JOC_ERR_STATE, stage::kRender, "binaural runtime is not open");
|
||||
}
|
||||
if (out == nullptr) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kRender, "null output");
|
||||
}
|
||||
out->clear();
|
||||
if (flush_samples == 0u) {
|
||||
return Status::success();
|
||||
}
|
||||
std::vector<double> chunk(static_cast<std::size_t>(kBlockSamples) * 2u, 0.0);
|
||||
std::uint32_t produced_total = 0;
|
||||
std::uint32_t remaining = flush_samples;
|
||||
while (remaining > 0) {
|
||||
const std::uint32_t request = std::min<std::uint32_t>(remaining, kBlockSamples);
|
||||
const int produced = ejoc_sofa_binaural_finish(handle_, request, chunk.data(), request);
|
||||
if (produced < 0) {
|
||||
const char* message = ejoc_sofa_binaural_last_error(handle_);
|
||||
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender,
|
||||
std::string("sofa finish failed: ") +
|
||||
(message != nullptr ? message : "unknown"));
|
||||
}
|
||||
if (produced == 0) {
|
||||
break;
|
||||
}
|
||||
out->insert(out->end(), chunk.begin(),
|
||||
chunk.begin() + static_cast<std::ptrdiff_t>(produced) * 2);
|
||||
produced_total += static_cast<std::uint32_t>(produced);
|
||||
remaining -= std::min<std::uint32_t>(remaining, static_cast<std::uint32_t>(produced));
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
} // namespace joc::binaural
|
||||
@@ -0,0 +1,96 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "eac3joc_core.h"
|
||||
#include "foundation/status.h"
|
||||
#include "hrtf/jochrtf.h"
|
||||
#include "oamd/oamd_parser.h"
|
||||
#include "timeline/position_timeline.h"
|
||||
|
||||
namespace joc::binaural {
|
||||
|
||||
// ADM direction of the LFE source, as the reference passes it.
|
||||
inline constexpr double kLfePosition[3] = {0.0, 1.0, 0.0};
|
||||
inline constexpr int kSourceCount = 16;
|
||||
inline constexpr std::uint32_t kBlockSamples = 512;
|
||||
|
||||
// Room constants the reference's native bridge passes for the default shoebox.
|
||||
struct RoomConstants {
|
||||
double dims[3] = {18.0, 18.0, 14.0};
|
||||
double listener[3] = {9.0, 9.0, 7.0};
|
||||
double walls[6] = {0.62, 0.60, 0.58, 0.61, 0.52, 0.56};
|
||||
double speed_of_sound = 343.3;
|
||||
std::uint32_t fdn_delays[4] = {1427u, 1783u, 1973u, 2099u};
|
||||
double fdn_feedback[4] = {0.7853685923259284, 0.7394299865898056, 0.7160221718631921,
|
||||
0.7009092068085467};
|
||||
double damping = 0.32;
|
||||
double fdn_output_gain = 0.22;
|
||||
std::uint32_t allpass_delays[2] = {113u, 331u};
|
||||
double allpass_gains[2] = {0.63, 0.51};
|
||||
std::uint32_t enable_early_reflections = 1;
|
||||
std::uint32_t enable_late_room = 1;
|
||||
};
|
||||
|
||||
enum class Profile : std::uint32_t { Near = 0, Mid = 1, Far = 2 };
|
||||
|
||||
bool profile_from_name(const char* name, Profile* out);
|
||||
|
||||
class SofaBinauralRuntime {
|
||||
public:
|
||||
SofaBinauralRuntime() = default;
|
||||
~SofaBinauralRuntime();
|
||||
|
||||
SofaBinauralRuntime(const SofaBinauralRuntime&) = delete;
|
||||
SofaBinauralRuntime& operator=(const SofaBinauralRuntime&) = delete;
|
||||
|
||||
Status open(const hrtf::Field& field, const hrtf::Kernels& kernels, Profile profile,
|
||||
const RoomConstants& room = RoomConstants{});
|
||||
|
||||
Status submit_frame(const float* objects16_planar, const oamd::OamdUpdate* update,
|
||||
std::int64_t frame_index, std::int64_t outer_sample_offset,
|
||||
std::int64_t object_delay_samples);
|
||||
|
||||
// Resets the kernel, the timeline and the counters (plan 31.2).
|
||||
Status reset();
|
||||
|
||||
// Drains the room tail. `flush_samples` is the drain length; the reference
|
||||
Status finish(std::uint32_t flush_samples, std::vector<double>* out);
|
||||
|
||||
// Program output (input minus the 961-sample kernel latency), interleaved.
|
||||
const std::vector<double>& output() const { return output_; }
|
||||
|
||||
void take_output(std::vector<double>* out) {
|
||||
out->swap(output_);
|
||||
output_.clear();
|
||||
}
|
||||
|
||||
std::uint64_t input_samples() const { return input_samples_; }
|
||||
std::uint64_t blocks_processed() const { return blocks_processed_; }
|
||||
std::size_t staged_samples() const { return staged_; }
|
||||
const timeline::OamdPositionTimeline& timeline() const { return timeline_; }
|
||||
|
||||
// finish_output_capacity as the reference computes it (plan 21.4).
|
||||
std::uint32_t finish_capacity(double tail_seconds) const;
|
||||
|
||||
private:
|
||||
Status process_block();
|
||||
|
||||
ejoc_sofa_binaural_handle handle_ = nullptr;
|
||||
Profile profile_ = Profile::Mid;
|
||||
timeline::OamdPositionTimeline timeline_;
|
||||
std::vector<double> staging_;
|
||||
std::size_t staged_ = 0;
|
||||
std::vector<double> block_output_;
|
||||
std::vector<double> output_;
|
||||
std::uint64_t input_samples_ = 0;
|
||||
std::uint64_t processed_samples_ = 0;
|
||||
std::uint64_t blocks_processed_ = 0;
|
||||
std::int64_t maximum_hrtf_delay_ = 0;
|
||||
std::uint32_t hrtf_history_slots_ = 1;
|
||||
std::uint32_t tail_samples_ = 61200;
|
||||
};
|
||||
|
||||
} // namespace joc::binaural
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,795 @@
|
||||
// joc_cli -- command line frontend, argument-compatible with the reference
|
||||
// Python CLI (main.py): the same positional input, the same mode selection
|
||||
// (ADM BWF by default, --speaker-layout or --binaural), the same option names,
|
||||
// choices and defaults, and the same default output naming under output/.
|
||||
//
|
||||
// Options that exist only because this build has no Python side or no Rosella
|
||||
// import chain (--backend python, --sofa-hrtf, --personalized-headphone,
|
||||
// metadata sidecars) fail with an explicit message instead of being ignored.
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "foundation/fs_utf8.h"
|
||||
#include "joc_core.h"
|
||||
|
||||
namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
namespace fs_utf8 = joc::fs_utf8;
|
||||
|
||||
constexpr double kRate = 48000.0;
|
||||
constexpr int kFrameSamples = 1536;
|
||||
|
||||
struct Options {
|
||||
std::string input;
|
||||
std::string output;
|
||||
std::string speaker_output;
|
||||
std::string binaural_output;
|
||||
std::string speaker_layout;
|
||||
bool binaural = false;
|
||||
std::string speaker_format = "float32";
|
||||
std::string binaural_format = "float32";
|
||||
std::string clip_action = "ask";
|
||||
int speaker_metadata_offset = 1473;
|
||||
std::string binaural_mode = "mid";
|
||||
std::string sofa_hrtf;
|
||||
std::string compiled_hrtf_cache;
|
||||
std::string personalized_headphone;
|
||||
bool personalized_headphone_used = false;
|
||||
std::string hrtf_cache_policy;
|
||||
std::string hrtf_cache_dir;
|
||||
double hrtf_radius_m = 1.0;
|
||||
double binaural_tail_seconds = 5.0;
|
||||
double binaural_tail_threshold = 1.0e-8;
|
||||
int binaural_chunk_frames = 64;
|
||||
double gain_db = 0.0;
|
||||
double duration = 0.0;
|
||||
bool duration_set = false;
|
||||
int object_delay_samples = 1473;
|
||||
std::string trajectory_mode = "compact";
|
||||
std::string ffmpeg;
|
||||
double eac3_drc_scale = 0.0;
|
||||
int eac3_target_level = 0;
|
||||
std::string backend = "auto";
|
||||
std::string native_library;
|
||||
int native_threads = 0;
|
||||
bool native_threads_set = false;
|
||||
std::string metadata_dir;
|
||||
std::string metadata_cache;
|
||||
std::string metadata_backend = "auto";
|
||||
std::string print_metadata = "none";
|
||||
std::string metadata_json;
|
||||
bool metadata_only = false;
|
||||
bool keep_raw = false;
|
||||
bool skip_sha256 = false;
|
||||
int progress_every = 1000;
|
||||
// C++-side additions (documented as such; the Python CLI has no equivalent).
|
||||
std::string bed;
|
||||
std::string kernels;
|
||||
std::string work_dir;
|
||||
std::string report_json;
|
||||
bool report_json_set = false;
|
||||
bool dry_run = false;
|
||||
bool quiet = false;
|
||||
bool help = false;
|
||||
};
|
||||
|
||||
const char* kLayoutChoices =
|
||||
"2.0 3.0 3.1 4.0 5.0 5.1 5.1.2 5.1.4 6.1 7.0 7.1 7.1.2 7.1.4 9.1.4 9.1.6 22.2";
|
||||
|
||||
void print_usage() {
|
||||
std::printf(
|
||||
"usage: joc_cli [options] input\n"
|
||||
"\n"
|
||||
"JustOneCacophony (JOC):E-AC-3 JOC → 25ch ADM BWF、扬声器 WAV 或双耳 WAV\n"
|
||||
"\n"
|
||||
"位置参数:\n"
|
||||
" input 输入 .m4a/.eac3/.ec3\n"
|
||||
"\n"
|
||||
"模式(默认输出 ADM BWF):\n"
|
||||
" --speaker-layout L 直接扬声器渲染布局,例如 2.0、5.1、7.1.2\n"
|
||||
" 可选值: %s\n"
|
||||
" --binaural 直接双耳渲染;不生成临时 ADM BWF\n"
|
||||
"\n"
|
||||
"输出:\n"
|
||||
" -o, --output PATH 输出文件;默认 output/<名称>.adm.wav、\n"
|
||||
" output/<名称>.<布局>.wav 或 output/<名称>.binaural.wav\n"
|
||||
" --speaker-output PATH 扬声器 WAV 路径;仅与 --speaker-layout 一起使用\n"
|
||||
" --binaural-output PATH 双耳 WAV 路径;仅与 --binaural 一起使用\n"
|
||||
" --speaker-format F 扬声器 WAV 格式 float32|int24,默认 float32\n"
|
||||
" --binaural-format F 双耳 WAV 格式 float32|int24,默认 float32\n"
|
||||
" --clip-action A int24 削波处理 ask|continue|float32|abort,默认 ask\n"
|
||||
"\n"
|
||||
"渲染:\n"
|
||||
" --speaker-metadata-offset N 扬声器渲染 metadata 相对帧偏移,默认 1473 samples\n"
|
||||
" --binaural-mode M 双耳渲染模式 off|near|mid|far,默认 mid;\n"
|
||||
" off 仅用于 ADM BWF(关闭 DBMD 双耳提示)\n"
|
||||
" --sofa-hrtf PATH SOFA SimpleFreeFieldHRIR 输入;.jochrtf 由本工具内部编译\n"
|
||||
" --personalized-headphone [PATH] Rosella 个性化模型,默认 "
|
||||
"HRTF/binaural.personalized_headphone\n"
|
||||
" --compiled-hrtf-cache PATH 直接读取 .jochrtf(高级用法,跳过 SOFA 编译)\n"
|
||||
" --hrtf-cache-policy P SOFA 编译缓存策略 none|memory|disk,默认 memory\n"
|
||||
" --hrtf-cache-dir DIR disk cache 目录,默认 <exe>/output/hrtf-cache\n"
|
||||
" --hrtf-radius-m R 选择最近的 SOFA measurement-radius shell,默认 1.0 m\n"
|
||||
" --binaural-tail-seconds S 双耳 room/filterbank flush 上限,默认 5 秒\n"
|
||||
" --binaural-tail-threshold T 双耳尾声裁切阈值,默认 1e-8;主体至少保留原时长\n"
|
||||
" --binaural-chunk-frames N 双耳内部批处理帧数,默认 64(本构建按 512 块渲染,\n"
|
||||
" 取值不影响输出)\n"
|
||||
" --gain-db X 成品增益 dB,默认 0;双耳路径以 float64 应用\n"
|
||||
" --duration S 只处理开头指定秒数\n"
|
||||
" --object-delay-samples N 对象 PCM/OAMD 时间补偿,默认 1473 samples\n"
|
||||
" --trajectory-mode M ADM 对象轨迹表示 compact|dense64,默认 compact\n"
|
||||
"\n"
|
||||
"输入与解码:\n"
|
||||
" --ffmpeg PATH ffmpeg 可执行文件,默认取 FFMPEG 环境变量或 PATH\n"
|
||||
" --eac3-drc-scale X E-AC-3 解码器 -drc_scale,0=关闭码流 dynrng,默认 0\n"
|
||||
" --eac3-target-level N E-AC-3 解码器 -target_level,0=不施加,默认 0\n"
|
||||
" --backend B JOC/扬声器 DSP 后端 auto|native;本构建无 python 后端\n"
|
||||
" --native-threads N 原生 DSP 总线程数;默认在 4 核以上使用 2\n"
|
||||
"\n"
|
||||
"诊断:\n"
|
||||
" --print-metadata M 诊断元数据输出 none|summary|frames,默认 none\n"
|
||||
" --metadata-json PATH 元数据汇总 JSON 路径\n"
|
||||
" --metadata-only 解析/打印元数据后退出\n"
|
||||
" --keep-raw 额外保留 16ch f32le 对象中间文件\n"
|
||||
" --skip-sha256 跳过最终文件 SHA-256 全量复扫\n"
|
||||
" --progress-every N 进度输出间隔,默认 1000 帧(渲染与收尾写盘同一节奏)\n"
|
||||
"\n"
|
||||
"本构建特有(Python 版没有对应参数):\n"
|
||||
" --bed PATH 已解码的 6 通道 float32 PCM;给出后不调用 ffmpeg 解码\n"
|
||||
" --kernels PATH 双耳滤波器组表 rosella_kernels.npz\n"
|
||||
" --work-dir DIR 临时目录\n"
|
||||
" --report-json PATH 结果 JSON 路径;默认 <输出>.report.json\n"
|
||||
" --dry-run 只校验配置\n"
|
||||
" --quiet 只输出警告与错误\n",
|
||||
kLayoutChoices);
|
||||
}
|
||||
|
||||
[[noreturn]] void fail(const std::string& message) { throw std::runtime_error(message); }
|
||||
|
||||
std::string require_value(const std::vector<std::string>& arguments, int* index) {
|
||||
if (static_cast<std::size_t>(*index) + 1u >= arguments.size()) {
|
||||
fail("argument " + arguments[static_cast<std::size_t>(*index)] +
|
||||
": expected one argument");
|
||||
}
|
||||
return arguments[static_cast<std::size_t>(++(*index))];
|
||||
}
|
||||
|
||||
double to_double(const std::string& text, const char* name) {
|
||||
try {
|
||||
std::size_t used = 0;
|
||||
const double value = std::stod(text, &used);
|
||||
if (used != text.size()) {
|
||||
fail(std::string(name) + ": invalid float value: " + text);
|
||||
}
|
||||
return value;
|
||||
} catch (const std::exception&) {
|
||||
fail(std::string(name) + ": invalid float value: " + text);
|
||||
}
|
||||
}
|
||||
|
||||
long long to_int(const std::string& text, const char* name) {
|
||||
try {
|
||||
std::size_t used = 0;
|
||||
const long long value = std::stoll(text, &used);
|
||||
if (used != text.size()) {
|
||||
fail(std::string(name) + ": invalid int value: " + text);
|
||||
}
|
||||
return value;
|
||||
} catch (const std::exception&) {
|
||||
fail(std::string(name) + ": invalid int value: " + text);
|
||||
}
|
||||
}
|
||||
|
||||
void check_choice(const std::string& value, const char* name,
|
||||
std::initializer_list<const char*> allowed) {
|
||||
for (const char* candidate : allowed) {
|
||||
if (value == candidate) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::string list;
|
||||
for (const char* candidate : allowed) {
|
||||
list += list.empty() ? candidate : (", " + std::string(candidate));
|
||||
}
|
||||
fail(std::string(name) + ": invalid choice: '" + value + "' (choose from " + list + ")");
|
||||
}
|
||||
|
||||
void parse_args(const std::vector<std::string>& arguments, Options* options) {
|
||||
std::vector<std::string> positional;
|
||||
const int argc = static_cast<int>(arguments.size());
|
||||
for (int index = 1; index < argc; ++index) {
|
||||
const std::string arg = arguments[static_cast<std::size_t>(index)];
|
||||
if (arg == "-h" || arg == "--help") { options->help = true; }
|
||||
else if (arg == "-o" || arg == "--output") { options->output = require_value(arguments, &index); }
|
||||
else if (arg == "--speaker-output") { options->speaker_output = require_value(arguments, &index); }
|
||||
else if (arg == "--binaural-output") { options->binaural_output = require_value(arguments, &index); }
|
||||
else if (arg == "--speaker-layout") { options->speaker_layout = require_value(arguments, &index); }
|
||||
else if (arg == "--binaural") { options->binaural = true; }
|
||||
else if (arg == "--speaker-format") { options->speaker_format = require_value(arguments, &index); }
|
||||
else if (arg == "--binaural-format") { options->binaural_format = require_value(arguments, &index); }
|
||||
else if (arg == "--clip-action") { options->clip_action = require_value(arguments, &index); }
|
||||
else if (arg == "--speaker-metadata-offset") {
|
||||
options->speaker_metadata_offset = static_cast<int>(
|
||||
to_int(require_value(arguments, &index), "--speaker-metadata-offset"));
|
||||
}
|
||||
else if (arg == "--binaural-mode") { options->binaural_mode = require_value(arguments, &index); }
|
||||
else if (arg == "--sofa-hrtf") { options->sofa_hrtf = require_value(arguments, &index); }
|
||||
else if (arg == "--compiled-hrtf-cache") { options->compiled_hrtf_cache = require_value(arguments, &index); }
|
||||
else if (arg == "--personalized-headphone") {
|
||||
options->personalized_headphone_used = true;
|
||||
// nargs="?": the path is optional, so the next token may be the input.
|
||||
// Without a path the executable-anchored default is resolved later.
|
||||
if (static_cast<std::size_t>(index) + 1u < arguments.size() &&
|
||||
arguments[static_cast<std::size_t>(index) + 1u][0] != '-') {
|
||||
options->personalized_headphone = require_value(arguments, &index);
|
||||
}
|
||||
}
|
||||
else if (arg == "--hrtf-cache-policy") { options->hrtf_cache_policy = require_value(arguments, &index); }
|
||||
else if (arg == "--hrtf-cache-dir") { options->hrtf_cache_dir = require_value(arguments, &index); }
|
||||
else if (arg == "--hrtf-radius-m") { options->hrtf_radius_m = to_double(require_value(arguments, &index), "--hrtf-radius-m"); }
|
||||
else if (arg == "--binaural-tail-seconds") { options->binaural_tail_seconds = to_double(require_value(arguments, &index), "--binaural-tail-seconds"); }
|
||||
else if (arg == "--binaural-tail-threshold") { options->binaural_tail_threshold = to_double(require_value(arguments, &index), "--binaural-tail-threshold"); }
|
||||
else if (arg == "--binaural-chunk-frames") { options->binaural_chunk_frames = static_cast<int>(to_int(require_value(arguments, &index), "--binaural-chunk-frames")); }
|
||||
else if (arg == "--gain-db") { options->gain_db = to_double(require_value(arguments, &index), "--gain-db"); }
|
||||
else if (arg == "--duration") { options->duration = to_double(require_value(arguments, &index), "--duration"); options->duration_set = true; }
|
||||
else if (arg == "--object-delay-samples") { options->object_delay_samples = static_cast<int>(to_int(require_value(arguments, &index), "--object-delay-samples")); }
|
||||
else if (arg == "--trajectory-mode") { options->trajectory_mode = require_value(arguments, &index); }
|
||||
else if (arg == "--ffmpeg") { options->ffmpeg = require_value(arguments, &index); }
|
||||
else if (arg == "--eac3-drc-scale") { options->eac3_drc_scale = to_double(require_value(arguments, &index), "--eac3-drc-scale"); }
|
||||
else if (arg == "--eac3-target-level") { options->eac3_target_level = static_cast<int>(to_int(require_value(arguments, &index), "--eac3-target-level")); }
|
||||
else if (arg == "--backend") { options->backend = require_value(arguments, &index); }
|
||||
else if (arg == "--native-library") { options->native_library = require_value(arguments, &index); }
|
||||
else if (arg == "--native-threads") { options->native_threads = static_cast<int>(to_int(require_value(arguments, &index), "--native-threads")); options->native_threads_set = true; }
|
||||
else if (arg == "--metadata-dir") { options->metadata_dir = require_value(arguments, &index); }
|
||||
else if (arg == "--metadata-cache") { options->metadata_cache = require_value(arguments, &index); }
|
||||
else if (arg == "--metadata-backend") { options->metadata_backend = require_value(arguments, &index); }
|
||||
else if (arg == "--print-metadata") { options->print_metadata = require_value(arguments, &index); }
|
||||
else if (arg == "--metadata-json") { options->metadata_json = require_value(arguments, &index); }
|
||||
else if (arg == "--metadata-only") { options->metadata_only = true; }
|
||||
else if (arg == "--keep-raw") { options->keep_raw = true; }
|
||||
else if (arg == "--skip-sha256") { options->skip_sha256 = true; }
|
||||
else if (arg == "--progress-every") { options->progress_every = static_cast<int>(to_int(require_value(arguments, &index), "--progress-every")); }
|
||||
else if (arg == "--bed") { options->bed = require_value(arguments, &index); }
|
||||
else if (arg == "--kernels") { options->kernels = require_value(arguments, &index); }
|
||||
else if (arg == "--work-dir") { options->work_dir = require_value(arguments, &index); }
|
||||
else if (arg == "--report-json") { options->report_json = require_value(arguments, &index); options->report_json_set = true; }
|
||||
else if (arg == "--dry-run") { options->dry_run = true; }
|
||||
else if (arg == "--quiet") { options->quiet = true; }
|
||||
else if (!arg.empty() && arg[0] == '-' && arg != "-") { fail("unrecognized argument: " + arg); }
|
||||
else { positional.push_back(arg); }
|
||||
}
|
||||
if (positional.size() > 1u) {
|
||||
fail("unrecognized extra arguments: " + positional[1] +
|
||||
(positional.size() > 2u ? " ..." : ""));
|
||||
}
|
||||
if (!positional.empty()) {
|
||||
options->input = positional.front();
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors the reference resolve_output(): <project>/output plus a mode-specific
|
||||
// name. The project directory is the executable's directory, as upstream uses
|
||||
// the script's directory, so the layout does not depend on the working directory.
|
||||
std::string resolve_output(const Options& options, const std::string& source,
|
||||
const std::string& executable_directory) {
|
||||
const std::string requested = !options.speaker_output.empty() ? options.speaker_output
|
||||
: !options.binaural_output.empty() ? options.binaural_output
|
||||
: options.output;
|
||||
if (!requested.empty()) {
|
||||
std::error_code error;
|
||||
const fs::path absolute = fs::absolute(fs_utf8::to_path(requested), error);
|
||||
return error ? requested : fs_utf8::from_path(absolute);
|
||||
}
|
||||
const fs::path directory = fs_utf8::to_path(executable_directory) / "output";
|
||||
const std::string stem = fs_utf8::from_path(fs_utf8::to_path(source).stem());
|
||||
if (!options.speaker_layout.empty()) {
|
||||
return fs_utf8::from_path(directory /
|
||||
fs_utf8::to_path(stem + "." + options.speaker_layout + ".wav"));
|
||||
}
|
||||
if (options.binaural) {
|
||||
return fs_utf8::from_path(directory / fs_utf8::to_path(stem + ".binaural.wav"));
|
||||
}
|
||||
return fs_utf8::from_path(directory / fs_utf8::to_path(stem + ".adm.wav"));
|
||||
}
|
||||
|
||||
// The project directory the reference anchors its defaults at: the directory of
|
||||
// the running executable, never the working directory.
|
||||
std::string executable_dir(const std::string& argv0) {
|
||||
const std::string own_path = fs_utf8::executable_path();
|
||||
if (!own_path.empty()) {
|
||||
const fs::path path = fs_utf8::to_path(own_path);
|
||||
if (path.has_parent_path()) {
|
||||
return fs_utf8::from_path(path.parent_path());
|
||||
}
|
||||
}
|
||||
if (argv0.empty()) {
|
||||
return ".";
|
||||
}
|
||||
std::error_code error;
|
||||
const fs::path path = fs::absolute(fs_utf8::to_path(argv0), error);
|
||||
if (error || path.empty()) {
|
||||
return ".";
|
||||
}
|
||||
return fs_utf8::from_path(path.parent_path());
|
||||
}
|
||||
|
||||
std::string find_kernels(const Options& options, const std::string& argv0) {
|
||||
(void)argv0;
|
||||
if (!options.kernels.empty() && !fs_utf8::exists(options.kernels)) {
|
||||
fail("--kernels 指向的文件不存在: " + options.kernels);
|
||||
}
|
||||
// Empty means the tables compiled into the library.
|
||||
return options.kernels;
|
||||
}
|
||||
|
||||
// Mirrors the reference binaural HRTF resolution (main.py:89-160): the SOFA file
|
||||
// is the user-facing input and the .jochrtf is only its compiled cache. Paths
|
||||
// are anchored at the executable directory, as the reference anchors them at the
|
||||
// project directory.
|
||||
struct HrtfInput {
|
||||
std::string sofa_path; // compile this
|
||||
std::string compiled_path; // or read this .jochrtf directly
|
||||
std::string cache_dir; // disk policy directory
|
||||
std::string personalized_path; // Rosella .personalized_headphone
|
||||
bool disk = false;
|
||||
};
|
||||
|
||||
std::string resolve_compiled_hrtf(const Options& options, const std::string& project_directory) {
|
||||
if (!options.compiled_hrtf_cache.empty()) {
|
||||
return options.compiled_hrtf_cache;
|
||||
}
|
||||
const std::string directory_utf8 =
|
||||
options.hrtf_cache_dir.empty()
|
||||
? fs_utf8::from_path(fs_utf8::to_path(project_directory) / "output" / "hrtf-cache")
|
||||
: options.hrtf_cache_dir;
|
||||
if (!fs_utf8::is_directory(directory_utf8)) {
|
||||
return std::string();
|
||||
}
|
||||
const fs::path directory = fs_utf8::to_path(directory_utf8);
|
||||
std::vector<fs::path> candidates;
|
||||
for (const fs::directory_entry& entry : fs::directory_iterator(directory)) {
|
||||
if (entry.is_regular_file() && entry.path().extension() == ".jochrtf") {
|
||||
candidates.push_back(entry.path());
|
||||
}
|
||||
}
|
||||
std::sort(candidates.begin(), candidates.end());
|
||||
if (candidates.size() > 1u) {
|
||||
fail(directory_utf8 +
|
||||
" 下有多个 .jochrtf 缓存,无法自动选择;请用 --sofa-hrtf PATH 或 "
|
||||
"--compiled-hrtf-cache PATH 显式指定");
|
||||
}
|
||||
return candidates.empty() ? std::string() : fs_utf8::from_path(candidates.front());
|
||||
}
|
||||
|
||||
HrtfInput resolve_hrtf_input(const Options& options, const std::string& project_directory) {
|
||||
HrtfInput input;
|
||||
const std::string default_sofa =
|
||||
fs_utf8::from_path(fs_utf8::to_path(project_directory) / "HRTF" / "binaural.sofa");
|
||||
const std::string default_private = fs_utf8::from_path(
|
||||
fs_utf8::to_path(project_directory) / "HRTF" / "binaural.personalized_headphone");
|
||||
const std::string default_cache_dir =
|
||||
fs_utf8::from_path(fs_utf8::to_path(project_directory) / "output" / "hrtf-cache");
|
||||
|
||||
if (!options.compiled_hrtf_cache.empty() && !options.hrtf_cache_policy.empty()) {
|
||||
fail("显式 .jochrtf 输入不能再指定 --hrtf-cache-policy");
|
||||
}
|
||||
if (!options.compiled_hrtf_cache.empty() && options.hrtf_radius_m != 1.0) {
|
||||
fail("显式 .jochrtf 输入不能再选择 SOFA radius shell");
|
||||
}
|
||||
if (options.personalized_headphone_used &&
|
||||
(!options.hrtf_cache_policy.empty() || !options.hrtf_cache_dir.empty() ||
|
||||
options.hrtf_radius_m != 1.0)) {
|
||||
fail("Rosella 模型输入不能使用 --hrtf-cache-policy/--hrtf-cache-dir/--hrtf-radius-m");
|
||||
}
|
||||
|
||||
std::string sofa = options.sofa_hrtf;
|
||||
std::string compiled = options.compiled_hrtf_cache;
|
||||
std::string personalized =
|
||||
options.personalized_headphone_used ? options.personalized_headphone : std::string();
|
||||
if (options.personalized_headphone_used && personalized.empty()) {
|
||||
// "--personalized-headphone" without a path means the project default.
|
||||
personalized = default_private;
|
||||
}
|
||||
if (sofa.empty() && compiled.empty() && personalized.empty()) {
|
||||
// The reference order: the SOFA file, then the unique compiled cache, then the
|
||||
// personalized model.
|
||||
if (fs_utf8::exists(default_sofa)) {
|
||||
sofa = default_sofa;
|
||||
} else {
|
||||
compiled = resolve_compiled_hrtf(options, project_directory);
|
||||
if (compiled.empty() && fs_utf8::exists(default_private)) {
|
||||
personalized = default_private;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!personalized.empty()) {
|
||||
if (!fs_utf8::exists(personalized)) {
|
||||
fail("双耳模型不存在: " + personalized);
|
||||
}
|
||||
input.personalized_path = personalized;
|
||||
return input;
|
||||
}
|
||||
if (sofa.empty() && compiled.empty()) {
|
||||
if (!options.hrtf_cache_policy.empty() || !options.hrtf_cache_dir.empty() ||
|
||||
options.hrtf_radius_m != 1.0) {
|
||||
fail("HRTF cache/radius 选项需要 --sofa-hrtf");
|
||||
}
|
||||
fail("--binaural 未找到 HRTF 输入:默认 " + default_sofa + "、" + default_private +
|
||||
" 或 " + default_cache_dir +
|
||||
" 下的 .jochrtf 都不存在,请用 --sofa-hrtf PATH、--personalized-headphone PATH "
|
||||
"或 --compiled-hrtf-cache PATH 指定");
|
||||
}
|
||||
if (sofa.empty() && (!options.hrtf_cache_policy.empty() || !options.hrtf_cache_dir.empty() ||
|
||||
options.hrtf_radius_m != 1.0)) {
|
||||
fail("HRTF cache/radius 选项需要 --sofa-hrtf");
|
||||
}
|
||||
if (sofa.empty()) {
|
||||
input.compiled_path = compiled;
|
||||
return input;
|
||||
}
|
||||
const std::string effective_policy =
|
||||
options.hrtf_cache_policy.empty() ? "memory" : options.hrtf_cache_policy;
|
||||
if (!options.hrtf_cache_dir.empty() && effective_policy != "disk") {
|
||||
fail("--hrtf-cache-dir 需要 SOFA 与 disk cache policy 一起使用");
|
||||
}
|
||||
input.sofa_path = sofa;
|
||||
input.disk = effective_policy == "disk";
|
||||
input.cache_dir = options.hrtf_cache_dir.empty() ? default_cache_dir : options.hrtf_cache_dir;
|
||||
if (!fs_utf8::exists(input.sofa_path)) {
|
||||
fail("SOFA HRTF 不存在: " + input.sofa_path);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
std::uint32_t binaural_mode_value(const std::string& name) {
|
||||
if (name == "off") { return JOC_BINAURAL_OFF; }
|
||||
if (name == "near") { return JOC_BINAURAL_NEAR; }
|
||||
if (name == "far") { return JOC_BINAURAL_FAR; }
|
||||
return JOC_BINAURAL_MID;
|
||||
}
|
||||
|
||||
std::uint32_t clip_action_value(const std::string& name) {
|
||||
if (name == "continue") { return JOC_CLIP_CONTINUE; }
|
||||
if (name == "float32") { return JOC_CLIP_FLOAT32; }
|
||||
if (name == "abort") { return JOC_CLIP_ABORT; }
|
||||
return JOC_CLIP_ASK;
|
||||
}
|
||||
|
||||
std::string format_eta(double seconds) {
|
||||
if (seconds < 0.0 || seconds > 86400.0) {
|
||||
return "--";
|
||||
}
|
||||
char buffer[64];
|
||||
std::snprintf(buffer, sizeof(buffer), "%.0fs", seconds);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
void JOC_CALL on_event(void* user, const joc_event* event) {
|
||||
const Options* options = static_cast<const Options*>(user);
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
switch (event->type) {
|
||||
case JOC_EV_PROGRESS: {
|
||||
if (options->quiet) {
|
||||
return;
|
||||
}
|
||||
const double fraction = event->progress >= 0.0 ? event->progress : 0.0;
|
||||
const double remaining =
|
||||
fraction > 0.0 ? event->elapsed_seconds * (1.0 - fraction) / fraction : -1.0;
|
||||
std::printf("[%s] %llu/%llu %.1fx realtime ETA %s\n", event->stage_name,
|
||||
static_cast<unsigned long long>(event->current_frame),
|
||||
static_cast<unsigned long long>(event->total_frames),
|
||||
event->realtime_factor, format_eta(remaining).c_str());
|
||||
std::fflush(stdout);
|
||||
return;
|
||||
}
|
||||
case JOC_EV_LOG: {
|
||||
if (options->quiet || event->log_level < JOC_LOG_INFO || event->message[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
std::printf("[%s] %s\n", event->stage_name, event->message);
|
||||
std::fflush(stdout);
|
||||
return;
|
||||
}
|
||||
case JOC_EV_WARNING:
|
||||
std::printf("[warning] %s\n", event->message);
|
||||
return;
|
||||
case JOC_EV_ERROR:
|
||||
std::fprintf(stderr, "[error] %s (%s)\n", event->message,
|
||||
joc_error_name(event->error_code));
|
||||
return;
|
||||
default:
|
||||
if (options->quiet || event->log_level < JOC_LOG_INFO || event->message[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
std::printf("[%s] %s\n", event->stage_name, event->message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
fs_utf8::configure_console();
|
||||
const std::vector<std::string> arguments = fs_utf8::command_line_arguments(argc, argv);
|
||||
Options options;
|
||||
try {
|
||||
parse_args(arguments, &options);
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "joc_cli: error: %s\n", error.what());
|
||||
return 2;
|
||||
}
|
||||
if (options.help) {
|
||||
print_usage();
|
||||
return 0;
|
||||
}
|
||||
if (options.input.empty()) {
|
||||
print_usage();
|
||||
return 2;
|
||||
}
|
||||
|
||||
try {
|
||||
check_choice(options.speaker_format, "--speaker-format", {"float32", "int24"});
|
||||
check_choice(options.binaural_format, "--binaural-format", {"float32", "int24"});
|
||||
check_choice(options.clip_action, "--clip-action",
|
||||
{"ask", "continue", "float32", "abort"});
|
||||
check_choice(options.binaural_mode, "--binaural-mode", {"off", "near", "mid", "far"});
|
||||
check_choice(options.trajectory_mode, "--trajectory-mode", {"compact", "dense64"});
|
||||
check_choice(options.backend, "--backend", {"auto", "native", "python"});
|
||||
check_choice(options.print_metadata, "--print-metadata", {"none", "summary", "frames"});
|
||||
check_choice(options.metadata_backend, "--metadata-backend", {"auto", "emdf", "sidecar"});
|
||||
if (!options.hrtf_cache_policy.empty()) {
|
||||
check_choice(options.hrtf_cache_policy, "--hrtf-cache-policy",
|
||||
{"none", "memory", "disk"});
|
||||
}
|
||||
|
||||
const bool speaker_mode = !options.speaker_layout.empty();
|
||||
const bool binaural_mode = options.binaural;
|
||||
if (speaker_mode && binaural_mode) {
|
||||
fail("argument --binaural: not allowed with argument --speaker-layout");
|
||||
}
|
||||
if (options.binaural_mode == "off" && (speaker_mode || binaural_mode)) {
|
||||
fail("--binaural-mode off 仅用于 ADM BWF 输出(关闭 DBMD 双耳提示);"
|
||||
"直接双耳渲染请使用 near/mid/far");
|
||||
}
|
||||
if (!options.speaker_output.empty() && !speaker_mode) {
|
||||
fail("--speaker-output 必须与 --speaker-layout 一起使用");
|
||||
}
|
||||
if (!options.binaural_output.empty() && !binaural_mode) {
|
||||
fail("--binaural-output 必须与 --binaural 一起使用");
|
||||
}
|
||||
const bool specific_output =
|
||||
!options.speaker_output.empty() || !options.binaural_output.empty();
|
||||
if (!options.output.empty() && specific_output) {
|
||||
fail("-o/--output 与 --speaker-output/--binaural-output 不能同时使用");
|
||||
}
|
||||
if (!options.speaker_output.empty() && !options.binaural_output.empty()) {
|
||||
fail("--speaker-output 与 --binaural-output 不能同时使用");
|
||||
}
|
||||
if (options.speaker_metadata_offset < 0) {
|
||||
fail("speaker-metadata-offset 不能为负数");
|
||||
}
|
||||
const bool hrtf_options_used =
|
||||
!options.sofa_hrtf.empty() || !options.compiled_hrtf_cache.empty() ||
|
||||
options.personalized_headphone_used || !options.hrtf_cache_policy.empty() ||
|
||||
!options.hrtf_cache_dir.empty() || options.hrtf_radius_m != 1.0;
|
||||
if (hrtf_options_used && !binaural_mode) {
|
||||
fail("SOFA/HRTF 选项仅与 --binaural 一起使用");
|
||||
}
|
||||
if (!std::isfinite(options.binaural_tail_seconds) ||
|
||||
options.binaural_tail_seconds < 0.0) {
|
||||
fail("binaural-tail-seconds 必须是非负有限值");
|
||||
}
|
||||
if (!std::isfinite(options.binaural_tail_threshold) ||
|
||||
options.binaural_tail_threshold < 0.0) {
|
||||
fail("binaural-tail-threshold 必须是非负有限值");
|
||||
}
|
||||
if (options.binaural_chunk_frames <= 0) {
|
||||
fail("binaural-chunk-frames 必须大于 0");
|
||||
}
|
||||
if (!std::isfinite(options.hrtf_radius_m) || options.hrtf_radius_m <= 0.0) {
|
||||
fail("hrtf-radius-m 必须是正有限值");
|
||||
}
|
||||
if (options.duration_set && options.duration <= 0.0) {
|
||||
fail("duration 必须大于 0");
|
||||
}
|
||||
if (options.object_delay_samples < 0) {
|
||||
fail("object-delay-samples 不能为负数");
|
||||
}
|
||||
if (options.native_threads_set && options.native_threads < 1) {
|
||||
fail("native-threads 必须大于 0");
|
||||
}
|
||||
if (!std::isfinite(options.gain_db) || std::abs(options.gain_db) > 200.0) {
|
||||
fail("gain-db 超出支持范围");
|
||||
}
|
||||
// Options this build cannot honour: fail loudly instead of ignoring them.
|
||||
if (options.backend == "python") {
|
||||
fail("--backend python 在本构建中不可用(已无 Python 后端);请使用 auto 或 native");
|
||||
}
|
||||
if (options.metadata_backend == "sidecar" || !options.metadata_dir.empty() ||
|
||||
!options.metadata_cache.empty()) {
|
||||
fail("metadata sidecar 在本构建中不可用(始终直接扫描 EMDF)");
|
||||
}
|
||||
if (!options.native_library.empty()) {
|
||||
std::fprintf(stderr, "[info] --native-library 在本构建中忽略(单一 joc_core.dll)\n");
|
||||
}
|
||||
if (!fs_utf8::exists(options.input)) {
|
||||
fail("输入文件不存在: " + options.input);
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "joc_cli: error: %s\n", error.what());
|
||||
return 2;
|
||||
}
|
||||
|
||||
const bool speaker_mode = !options.speaker_layout.empty();
|
||||
const bool binaural_mode = options.binaural;
|
||||
const std::string project_directory =
|
||||
executable_dir(arguments.empty() ? std::string() : arguments.front());
|
||||
const std::string output_path = resolve_output(options, options.input, project_directory);
|
||||
std::error_code directory_error;
|
||||
fs::create_directories(fs_utf8::to_path(output_path).parent_path(), directory_error);
|
||||
|
||||
joc_task_config config{};
|
||||
config.struct_size = sizeof(config);
|
||||
config.struct_version = JOC_TASK_CONFIG_VERSION;
|
||||
config.input_path = options.input.c_str();
|
||||
config.output_path = output_path.c_str();
|
||||
config.ffmpeg_path = options.ffmpeg.empty() ? nullptr : options.ffmpeg.c_str();
|
||||
config.bed_path = options.bed.empty() ? nullptr : options.bed.c_str();
|
||||
config.work_dir = options.work_dir.empty() ? nullptr : options.work_dir.c_str();
|
||||
config.eac3_drc_scale = options.eac3_drc_scale;
|
||||
config.eac3_target_level = options.eac3_target_level;
|
||||
config.operation =
|
||||
binaural_mode ? JOC_OP_BINAURAL : speaker_mode ? JOC_OP_SPEAKER : JOC_OP_ADM_BWF;
|
||||
const std::string& requested_format =
|
||||
binaural_mode ? options.binaural_format : options.speaker_format;
|
||||
config.output_format =
|
||||
requested_format == "int24" ? JOC_FORMAT_PCM24 : JOC_FORMAT_FLOAT32;
|
||||
config.clip_action = clip_action_value(options.clip_action);
|
||||
config.speaker_layout_name = speaker_mode ? options.speaker_layout.c_str() : nullptr;
|
||||
config.speaker_metadata_offset = static_cast<std::uint32_t>(options.speaker_metadata_offset);
|
||||
config.binaural_mode = binaural_mode_value(options.binaural_mode);
|
||||
config.adm_binaural_mode = binaural_mode_value(options.binaural_mode);
|
||||
config.binaural_tail_seconds = options.binaural_tail_seconds;
|
||||
config.binaural_tail_threshold = options.binaural_tail_threshold;
|
||||
config.binaural_chunk_frames = static_cast<std::uint32_t>(options.binaural_chunk_frames);
|
||||
config.object_delay_samples = static_cast<std::uint32_t>(options.object_delay_samples);
|
||||
config.trajectory_mode =
|
||||
options.trajectory_mode == "dense64" ? JOC_TRAJECTORY_DENSE64 : JOC_TRAJECTORY_COMPACT;
|
||||
config.gain_db = options.gain_db;
|
||||
config.progress_interval_frames = static_cast<std::uint32_t>(options.progress_every);
|
||||
config.native_threads =
|
||||
options.native_threads_set ? static_cast<std::uint32_t>(options.native_threads) : 0u;
|
||||
config.print_metadata = options.print_metadata == "frames" ? 2u
|
||||
: options.print_metadata == "summary" ? 1u
|
||||
: 0u;
|
||||
config.metadata_json_path =
|
||||
options.metadata_json.empty() ? nullptr : options.metadata_json.c_str();
|
||||
config.duration_frames =
|
||||
options.duration_set
|
||||
? static_cast<std::uint64_t>(
|
||||
std::ceil(options.duration * kRate / static_cast<double>(kFrameSamples)))
|
||||
: 0u;
|
||||
config.flags = 0u;
|
||||
if (options.skip_sha256) { config.flags |= JOC_TASK_F_SKIP_SHA256; }
|
||||
if (options.keep_raw) { config.flags |= JOC_TASK_F_KEEP_INTERMEDIATE; }
|
||||
if (options.metadata_only) { config.flags |= JOC_TASK_F_METADATA_ONLY; }
|
||||
if (options.quiet) { config.flags |= JOC_TASK_F_QUIET; }
|
||||
|
||||
std::string hrtf_path;
|
||||
std::string hrtf_sofa_path;
|
||||
std::string hrtf_cache_dir;
|
||||
std::string personalized_path;
|
||||
std::string kernels_path;
|
||||
if (binaural_mode) {
|
||||
try {
|
||||
const HrtfInput input = resolve_hrtf_input(options, project_directory);
|
||||
hrtf_path = input.compiled_path;
|
||||
hrtf_sofa_path = input.sofa_path;
|
||||
hrtf_cache_dir = input.cache_dir;
|
||||
personalized_path = input.personalized_path;
|
||||
kernels_path = find_kernels(options, arguments.empty() ? std::string()
|
||||
: arguments.front());
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "joc_cli: error: %s\n", error.what());
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
config.hrtf_path = hrtf_path.empty() ? nullptr : hrtf_path.c_str();
|
||||
config.hrtf_sofa_path = hrtf_sofa_path.empty() ? nullptr : hrtf_sofa_path.c_str();
|
||||
config.hrtf_cache_dir = hrtf_cache_dir.empty() ? nullptr : hrtf_cache_dir.c_str();
|
||||
config.personalized_headphone_path =
|
||||
personalized_path.empty() ? nullptr : personalized_path.c_str();
|
||||
config.hrtf_cache_policy = options.hrtf_cache_policy == "disk" ? JOC_HRTF_CACHE_DISK
|
||||
: options.hrtf_cache_policy == "none" ? JOC_HRTF_CACHE_NONE
|
||||
: JOC_HRTF_CACHE_MEMORY;
|
||||
config.hrtf_radius_m = options.hrtf_radius_m;
|
||||
config.kernels_path = kernels_path.empty() ? nullptr : kernels_path.c_str();
|
||||
|
||||
joc_validation_issue issues[32];
|
||||
std::uint32_t issue_count = 0;
|
||||
const joc_error validated = joc_task_validate(&config, issues, 32u, &issue_count);
|
||||
for (std::uint32_t index = 0; index < std::min(issue_count, 32u); ++index) {
|
||||
if (issues[index].severity >= 2u) {
|
||||
std::fprintf(stderr, "[error] %s: %s\n", issues[index].field, issues[index].message);
|
||||
} else if (!options.quiet) {
|
||||
std::fprintf(stderr, "[warning] %s: %s\n", issues[index].field,
|
||||
issues[index].message);
|
||||
}
|
||||
}
|
||||
if (validated != JOC_OK) {
|
||||
std::fprintf(stderr, "joc_cli: error: configuration rejected (%u issue(s))\n", issue_count);
|
||||
return 2;
|
||||
}
|
||||
if (options.dry_run) {
|
||||
std::printf("configuration accepted (%u issue(s))\n", issue_count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
joc_event_sink sink{};
|
||||
sink.struct_size = sizeof(sink);
|
||||
sink.callback = &on_event;
|
||||
sink.user = &options;
|
||||
|
||||
if (!options.quiet) {
|
||||
const char* mode_name = binaural_mode ? "binaural" : speaker_mode ? "speaker" : "adm";
|
||||
std::printf("[cli] %s -> %s (%s)\n", options.input.c_str(), output_path.c_str(),
|
||||
mode_name);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
joc_task_result result{};
|
||||
const joc_error status = joc_task_execute(&config, &sink, &result);
|
||||
|
||||
const std::string report_path =
|
||||
options.report_json_set ? options.report_json : (output_path + ".report.json");
|
||||
{
|
||||
std::size_t needed = 0;
|
||||
joc_task_result_to_json(&result, nullptr, 0u, &needed);
|
||||
std::vector<char> buffer(needed + 1u);
|
||||
if (joc_task_result_to_json(&result, buffer.data(), buffer.size(), &needed) == JOC_OK) {
|
||||
if (std::FILE* file = fs_utf8::fopen(report_path, "wb")) {
|
||||
std::fwrite(buffer.data(), 1, std::strlen(buffer.data()), file);
|
||||
std::fputc('\n', file);
|
||||
std::fclose(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::printf("\nresult: %s\n", status == JOC_OK ? "ok" : joc_error_name(status));
|
||||
std::printf(" output : %s\n", output_path.c_str());
|
||||
std::printf(" frames : %llu (%.2f s)\n",
|
||||
static_cast<unsigned long long>(result.input_frames), result.duration_sec);
|
||||
std::printf(" output samples: %llu\n",
|
||||
static_cast<unsigned long long>(result.output_samples));
|
||||
std::printf(" output bytes : %llu\n",
|
||||
static_cast<unsigned long long>(result.output_file_bytes));
|
||||
std::printf(" format : %s\n",
|
||||
result.output_format_actual == JOC_FORMAT_PCM24 ? "int24" : "float32");
|
||||
std::printf(" peak : %.9g (%llu sample(s) above full scale)\n", result.output_peak,
|
||||
static_cast<unsigned long long>(result.output_over_unity_values));
|
||||
std::printf(" sha256 : %s\n",
|
||||
result.output_sha256[0] != '\0' ? result.output_sha256 : "(skipped)");
|
||||
std::printf(" report : %s\n", report_path.c_str());
|
||||
// Each stage time is measured where that stage actually runs, and the three
|
||||
// stages now overlap (see the pipeline in src/task/task.cpp), so the stage
|
||||
// times deliberately do not add up to the wall-clock total.
|
||||
std::printf(" timings : decode %.2fs, joc %.2fs, dsp %.2fs, write %.2fs"
|
||||
" (stage times, concurrent), total %.2fs\n",
|
||||
result.t_decode_bed, result.t_render, result.t_render_dsp, result.t_write_file,
|
||||
result.t_total);
|
||||
if (status != JOC_OK) {
|
||||
std::fprintf(stderr, "joc_cli: error: %s: %s\n", result.error_stage, result.error_message);
|
||||
}
|
||||
return status == JOC_OK ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
#include "eac3_transport/eac3_reader.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "foundation/status.h"
|
||||
|
||||
namespace joc::eac3 {
|
||||
|
||||
namespace {
|
||||
constexpr std::size_t kHeaderBytes = 4;
|
||||
} // namespace
|
||||
|
||||
void FrameReader::push(const std::uint8_t* data, std::size_t size) {
|
||||
if (failed_ || data == nullptr || size == 0) {
|
||||
return;
|
||||
}
|
||||
if (consumed_ > 0) {
|
||||
compact();
|
||||
}
|
||||
buffer_.insert(buffer_.end(), data, data + size);
|
||||
}
|
||||
|
||||
void FrameReader::compact() {
|
||||
if (consumed_ == 0) {
|
||||
return;
|
||||
}
|
||||
buffer_.erase(buffer_.begin(), buffer_.begin() + static_cast<std::ptrdiff_t>(consumed_));
|
||||
base_offset_ += consumed_;
|
||||
consumed_ = 0;
|
||||
}
|
||||
|
||||
void FrameReader::fail(joc_error code, std::string message) {
|
||||
failed_ = true;
|
||||
error_ = code;
|
||||
message_ = std::move(message);
|
||||
}
|
||||
|
||||
FrameReader::Next FrameReader::next(Frame* out) {
|
||||
if (failed_) {
|
||||
return Next::Fail;
|
||||
}
|
||||
const std::size_t available = buffer_.size() - consumed_;
|
||||
if (available == 0) {
|
||||
return Next::End;
|
||||
}
|
||||
const std::uint8_t* p = buffer_.data() + consumed_;
|
||||
|
||||
// The reference implementation rejects a frame whose header does not fit,
|
||||
// rather than silently resynchronising on the next 0x0B77.
|
||||
if (available < kHeaderBytes) {
|
||||
if (finished_) {
|
||||
fail(JOC_ERR_EAC3_SYNCFRAME, "E-AC-3 syncframe header truncated at end of input");
|
||||
return Next::Fail;
|
||||
}
|
||||
return Next::End;
|
||||
}
|
||||
|
||||
const std::uint16_t syncword = static_cast<std::uint16_t>((static_cast<std::uint16_t>(p[0]) << 8) | p[1]);
|
||||
if (syncword != kSyncword) {
|
||||
fail(JOC_ERR_EAC3_SYNCFRAME, "invalid E-AC-3 syncword (silent resynchronisation is not allowed)");
|
||||
return Next::Fail;
|
||||
}
|
||||
|
||||
// frmsiz: 11 bits spread over the low 3 bits of byte 2 and all of byte 3,
|
||||
const std::size_t words =
|
||||
static_cast<std::size_t>(((p[2] & 0x07u) << 8) | p[3]) + 1u;
|
||||
const std::size_t frame_bytes = words * 2u;
|
||||
|
||||
if (frame_bytes > available) {
|
||||
if (!finished_) {
|
||||
return Next::End;
|
||||
}
|
||||
fail(JOC_ERR_BITSTREAM_TRUNCATED,
|
||||
"last E-AC-3 syncframe extends past end of input (declared " +
|
||||
std::to_string(frame_bytes) + " bytes, remaining " +
|
||||
std::to_string(available) + ")");
|
||||
return Next::Fail;
|
||||
}
|
||||
|
||||
if (out != nullptr) {
|
||||
out->data = p;
|
||||
out->size = frame_bytes;
|
||||
out->offset = base_offset_ + consumed_;
|
||||
}
|
||||
consumed_ += frame_bytes;
|
||||
stream_offset_ = base_offset_ + consumed_;
|
||||
++frames_emitted_;
|
||||
return Next::Ok;
|
||||
}
|
||||
|
||||
joc_error FrameReader::frame_bytes(const std::uint8_t* data, std::size_t size, std::size_t offset,
|
||||
std::size_t* out_frame_bytes) {
|
||||
if (data == nullptr || out_frame_bytes == nullptr) {
|
||||
return JOC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (offset + kHeaderBytes > size) {
|
||||
return JOC_ERR_EAC3_SYNCFRAME;
|
||||
}
|
||||
if (static_cast<std::uint16_t>((static_cast<std::uint16_t>(data[offset]) << 8) | data[offset + 1]) !=
|
||||
kSyncword) {
|
||||
return JOC_ERR_EAC3_SYNCFRAME;
|
||||
}
|
||||
const std::size_t words =
|
||||
static_cast<std::size_t>(((data[offset + 2] & 0x07u) << 8) | data[offset + 3]) + 1u;
|
||||
const std::size_t frame_bytes = words * 2u;
|
||||
if (offset + frame_bytes > size) {
|
||||
return JOC_ERR_BITSTREAM_TRUNCATED;
|
||||
}
|
||||
*out_frame_bytes = frame_bytes;
|
||||
return JOC_OK;
|
||||
}
|
||||
|
||||
} // namespace joc::eac3
|
||||
@@ -0,0 +1,68 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "joc_core.h"
|
||||
|
||||
namespace joc::eac3 {
|
||||
|
||||
struct Frame {
|
||||
const std::uint8_t* data = nullptr;
|
||||
std::size_t size = 0;
|
||||
std::size_t offset = 0; // byte offset of the frame start in the fed stream
|
||||
};
|
||||
|
||||
class FrameReader {
|
||||
public:
|
||||
enum class Next {
|
||||
Ok,
|
||||
End,
|
||||
Fail
|
||||
};
|
||||
|
||||
FrameReader() = default;
|
||||
FrameReader(const std::uint8_t* data, std::size_t size) {
|
||||
push(data, size);
|
||||
finish();
|
||||
}
|
||||
|
||||
// Appends bytes to the internal buffer (used in incremental mode).
|
||||
void push(const std::uint8_t* data, std::size_t size);
|
||||
|
||||
// Declares that no further bytes will arrive; a frame that is still
|
||||
void finish() { finished_ = true; }
|
||||
|
||||
Next next(Frame* out);
|
||||
|
||||
joc_error error() const { return error_; }
|
||||
const std::string& error_message() const { return message_; }
|
||||
|
||||
std::size_t frames_emitted() const { return frames_emitted_; }
|
||||
std::size_t stream_offset() const { return stream_offset_; }
|
||||
|
||||
// report its declared byte length.
|
||||
static joc_error frame_bytes(const std::uint8_t* data, std::size_t size, std::size_t offset,
|
||||
std::size_t* out_frame_bytes);
|
||||
|
||||
static constexpr std::uint16_t kSyncword = 0x0B77;
|
||||
|
||||
private:
|
||||
void compact();
|
||||
void fail(joc_error code, std::string message);
|
||||
|
||||
std::vector<std::uint8_t> buffer_;
|
||||
std::size_t consumed_ = 0; // bytes of buffer_ already turned into frames
|
||||
std::size_t base_offset_ = 0;
|
||||
bool finished_ = false;
|
||||
bool failed_ = false;
|
||||
joc_error error_ = JOC_OK;
|
||||
std::string message_;
|
||||
std::size_t frames_emitted_ = 0;
|
||||
std::size_t stream_offset_ = 0;
|
||||
};
|
||||
|
||||
} // namespace joc::eac3
|
||||
@@ -0,0 +1,305 @@
|
||||
#include "emdf/emdf_parser.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#include "foundation/bit_reader.h"
|
||||
|
||||
namespace joc::emdf {
|
||||
|
||||
namespace {
|
||||
|
||||
Status syntax_fail(const std::string& message) {
|
||||
return Status::fail(JOC_ERR_EMDF_SYNTAX, stage::kEmdf, message);
|
||||
}
|
||||
|
||||
Status truncated_fail(const bits::BitReader& reader) {
|
||||
return Status::fail(JOC_ERR_BITSTREAM_TRUNCATED, stage::kEmdf,
|
||||
std::string("EMDF bitstream truncated: ") + reader.error_message());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status parse_at(const std::uint8_t* data, std::size_t size, std::size_t start_bit, Container* out) {
|
||||
if (data == nullptr || out == nullptr) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kEmdf, "null buffer or output");
|
||||
}
|
||||
if (start_bit + 16u > size * 8u) {
|
||||
return syntax_fail("EMDF syncword position beyond buffer");
|
||||
}
|
||||
|
||||
bits::BitReader reader;
|
||||
reader.reset(data, size, start_bit);
|
||||
|
||||
if (reader.read(16) != kSyncword) {
|
||||
return syntax_fail("EMDF syncword mismatch at bit " + std::to_string(start_bit));
|
||||
}
|
||||
const std::uint32_t length = reader.read(16);
|
||||
const std::size_t body_start = reader.position();
|
||||
const std::size_t body_end = body_start + static_cast<std::size_t>(length) * 8u;
|
||||
if (body_end > reader.limit()) {
|
||||
return syntax_fail("EMDF container length " + std::to_string(length) +
|
||||
" exceeds buffer at bit " + std::to_string(start_bit));
|
||||
}
|
||||
reader.set_limit_bits(body_end);
|
||||
|
||||
std::uint32_t version = reader.read(2);
|
||||
if (version == 3u) {
|
||||
std::uint32_t extra = 0;
|
||||
if (!bits::variable_bits(reader, 2, 8, &extra)) {
|
||||
return reader.error() == JOC_ERR_BITSTREAM_TRUNCATED ? truncated_fail(reader)
|
||||
: syntax_fail(reader.error_message());
|
||||
}
|
||||
version += extra;
|
||||
}
|
||||
std::uint32_t key_id = reader.read(3);
|
||||
if (key_id == 7u) {
|
||||
std::uint32_t extra = 0;
|
||||
if (!bits::variable_bits(reader, 3, 8, &extra)) {
|
||||
return reader.error() == JOC_ERR_BITSTREAM_TRUNCATED ? truncated_fail(reader)
|
||||
: syntax_fail(reader.error_message());
|
||||
}
|
||||
key_id += extra;
|
||||
}
|
||||
if (reader.failed()) {
|
||||
return truncated_fail(reader);
|
||||
}
|
||||
// TS 103 420 JOC uses version 0 / key_id 0; the strict check also rejects
|
||||
// false 0x5838 markers that happen to sit inside audio data.
|
||||
if (version != 0u || key_id != 0u) {
|
||||
return syntax_fail("unsupported EMDF version/key_id " + std::to_string(version) + "/" +
|
||||
std::to_string(key_id));
|
||||
}
|
||||
|
||||
Container container;
|
||||
container.start_bit = start_bit;
|
||||
bool terminated = false;
|
||||
|
||||
while (reader.position() + 5u <= body_end) {
|
||||
std::uint32_t payload_id = reader.read(5);
|
||||
if (reader.failed()) {
|
||||
return truncated_fail(reader);
|
||||
}
|
||||
if (payload_id == 0u) {
|
||||
terminated = true;
|
||||
break;
|
||||
}
|
||||
if (payload_id == 0x1Fu) {
|
||||
std::uint32_t extra = 0;
|
||||
if (!bits::variable_bits(reader, 5, 8, &extra)) {
|
||||
return reader.error() == JOC_ERR_BITSTREAM_TRUNCATED ? truncated_fail(reader)
|
||||
: syntax_fail(reader.error_message());
|
||||
}
|
||||
payload_id += extra;
|
||||
}
|
||||
for (std::size_t i = 0; i < container.payload_count; ++i) {
|
||||
if (container.payloads[i].id == static_cast<std::uint8_t>(payload_id)) {
|
||||
return syntax_fail("duplicate EMDF payload id " + std::to_string(payload_id));
|
||||
}
|
||||
}
|
||||
if (container.payload_count >= kMaxPayloads) {
|
||||
return syntax_fail("EMDF payload count exceeds " + std::to_string(kMaxPayloads));
|
||||
}
|
||||
|
||||
const std::uint32_t has_sample_offset = reader.read(1);
|
||||
std::uint16_t sample_offset = 0;
|
||||
if (has_sample_offset != 0u) {
|
||||
sample_offset = static_cast<std::uint16_t>(reader.read(12) >> 1);
|
||||
}
|
||||
if (reader.read(1) != 0u) {
|
||||
std::uint32_t ignored = 0;
|
||||
if (!bits::variable_bits(reader, 11, 8, &ignored)) {
|
||||
return reader.error() == JOC_ERR_BITSTREAM_TRUNCATED ? truncated_fail(reader)
|
||||
: syntax_fail(reader.error_message());
|
||||
}
|
||||
}
|
||||
if (reader.read(1) != 0u) {
|
||||
std::uint32_t ignored = 0;
|
||||
if (!bits::variable_bits(reader, 2, 8, &ignored)) {
|
||||
return reader.error() == JOC_ERR_BITSTREAM_TRUNCATED ? truncated_fail(reader)
|
||||
: syntax_fail(reader.error_message());
|
||||
}
|
||||
}
|
||||
if (reader.read(1) != 0u) {
|
||||
if (!reader.skip(8)) {
|
||||
return truncated_fail(reader);
|
||||
}
|
||||
}
|
||||
if (reader.read(1) == 0u) {
|
||||
bool frame_aligned = false;
|
||||
if (has_sample_offset == 0u) {
|
||||
frame_aligned = reader.read(1) != 0u;
|
||||
if (frame_aligned) {
|
||||
if (!reader.skip(2)) {
|
||||
return truncated_fail(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (has_sample_offset != 0u || frame_aligned) {
|
||||
if (!reader.skip(7)) {
|
||||
return truncated_fail(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reader.failed()) {
|
||||
return truncated_fail(reader);
|
||||
}
|
||||
|
||||
std::uint32_t payload_size = 0;
|
||||
if (!bits::variable_bits(reader, 8, 8, &payload_size)) {
|
||||
return reader.error() == JOC_ERR_BITSTREAM_TRUNCATED ? truncated_fail(reader)
|
||||
: syntax_fail(reader.error_message());
|
||||
}
|
||||
const std::size_t payload_bits = static_cast<std::size_t>(payload_size) * 8u;
|
||||
if (reader.position() + payload_bits > body_end) {
|
||||
return syntax_fail("EMDF payload id " + std::to_string(payload_id) +
|
||||
" extends past container body (size " + std::to_string(payload_size) +
|
||||
" at bit " + std::to_string(reader.position()) + ")");
|
||||
}
|
||||
|
||||
Payload& entry = container.payloads[container.payload_count++];
|
||||
entry.id = static_cast<std::uint8_t>(payload_id);
|
||||
entry.sample_offset = sample_offset;
|
||||
entry.bit_offset = reader.position();
|
||||
entry.size = payload_size;
|
||||
|
||||
if (!reader.skip(payload_bits)) {
|
||||
return truncated_fail(reader);
|
||||
}
|
||||
}
|
||||
|
||||
if (!terminated) {
|
||||
return syntax_fail("EMDF container has no payload id 0 terminator");
|
||||
}
|
||||
container.raw_size = 4u + static_cast<std::size_t>(length);
|
||||
*out = container;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
void marker_offsets(const std::uint8_t* data, std::size_t size, std::vector<std::size_t>* out) {
|
||||
out->clear();
|
||||
if (data == nullptr || size < 4u) {
|
||||
return;
|
||||
}
|
||||
// Eight global bit alignments. For shift != 0 the reference builds an
|
||||
// (n-1)-byte shifted view and only scans pairs inside it, which is what the
|
||||
// bounds below reproduce exactly.
|
||||
for (std::size_t shift = 0; shift < 8u; ++shift) {
|
||||
const std::size_t aligned_len = (shift == 0u) ? size : (size - 1u);
|
||||
auto aligned_byte = [&](std::size_t index) -> std::uint8_t {
|
||||
if (shift == 0u) {
|
||||
return data[index];
|
||||
}
|
||||
const std::uint16_t high = static_cast<std::uint16_t>(data[index]) << shift;
|
||||
const std::uint16_t low = static_cast<std::uint16_t>(data[index + 1u]) >> (8u - shift);
|
||||
return static_cast<std::uint8_t>((high | low) & 0xFFu);
|
||||
};
|
||||
if (aligned_len < 2u) {
|
||||
continue;
|
||||
}
|
||||
for (std::size_t i = 0; i + 1u < aligned_len; ++i) {
|
||||
if (aligned_byte(i) == 0x58u && aligned_byte(i + 1u) == 0x38u) {
|
||||
out->push_back(i * 8u + shift);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(out->begin(), out->end());
|
||||
}
|
||||
|
||||
Status find_joc_emdf(const std::uint8_t* data, std::size_t size, Container* out) {
|
||||
if (data == nullptr || out == nullptr) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kEmdf, "null buffer or output");
|
||||
}
|
||||
std::vector<std::size_t> offsets;
|
||||
marker_offsets(data, size, &offsets);
|
||||
|
||||
std::vector<Container> matches;
|
||||
std::size_t parse_errors = 0;
|
||||
std::string first_parse_error;
|
||||
for (const std::size_t start_bit : offsets) {
|
||||
Container candidate;
|
||||
const Status status = parse_at(data, size, start_bit, &candidate);
|
||||
if (!status.ok()) {
|
||||
++parse_errors;
|
||||
if (first_parse_error.empty()) {
|
||||
first_parse_error = "@bit" + std::to_string(start_bit) + ": " + status.message();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (candidate.find(kIdOamd) != nullptr && candidate.find(kIdJoc) != nullptr) {
|
||||
matches.push_back(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.empty()) {
|
||||
// Classification stays at the transport level (identical to the reference
|
||||
// implementation, which raises emdf_transport here), but the underlying
|
||||
std::string message =
|
||||
"no contiguous EMDF container carrying ID11+ID14 in this syncframe (markers=" +
|
||||
std::to_string(offsets.size()) + ", parse_failures=" + std::to_string(parse_errors) +
|
||||
")";
|
||||
if (!first_parse_error.empty()) {
|
||||
message += "; first candidate error " + first_parse_error;
|
||||
}
|
||||
return Status::fail(JOC_ERR_EMDF_TRANSPORT, stage::kEmdf, message);
|
||||
}
|
||||
|
||||
std::sort(matches.begin(), matches.end(),
|
||||
[](const Container& a, const Container& b) { return a.start_bit < b.start_bit; });
|
||||
|
||||
// A payload may contain bytes that look like another 0x5838 container; a
|
||||
std::vector<Container> top_level;
|
||||
for (const Container& candidate : matches) {
|
||||
bool nested = false;
|
||||
for (const Container& parent : top_level) {
|
||||
if (parent.start_bit < candidate.start_bit &&
|
||||
candidate.start_bit < parent.start_bit + parent.raw_size * 8u) {
|
||||
nested = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!nested) {
|
||||
top_level.push_back(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
if (top_level.size() != 1u) {
|
||||
return Status::fail(JOC_ERR_EMDF_TRANSPORT, stage::kEmdf,
|
||||
"multiple top-level JOC EMDF containers (" +
|
||||
std::to_string(top_level.size()) +
|
||||
"); automatic selection is not defined");
|
||||
}
|
||||
*out = top_level.front();
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
void extract_container_bytes(const std::uint8_t* data, std::size_t size, const Container& container,
|
||||
std::vector<std::uint8_t>* out) {
|
||||
out->assign(container.raw_size, 0u);
|
||||
if (out->empty()) {
|
||||
return;
|
||||
}
|
||||
bits::BitReader reader;
|
||||
reader.reset(data, size, container.start_bit);
|
||||
reader.read_bytes(out->data(), out->size());
|
||||
}
|
||||
|
||||
Status extract_payload_bytes(const std::uint8_t* data, std::size_t size, const Payload& payload,
|
||||
std::vector<std::uint8_t>* out) {
|
||||
if (data == nullptr || out == nullptr) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kEmdf, "null buffer or output");
|
||||
}
|
||||
out->assign(payload.size, 0u);
|
||||
if (out->empty()) {
|
||||
return Status::success();
|
||||
}
|
||||
bits::BitReader reader;
|
||||
reader.reset(data, size, payload.bit_offset);
|
||||
if (!reader.read_bytes(out->data(), out->size())) {
|
||||
return Status::fail(JOC_ERR_BITSTREAM_TRUNCATED, stage::kEmdf,
|
||||
"payload bytes extend past the syncframe");
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
} // namespace joc::emdf
|
||||
@@ -0,0 +1,57 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "joc_core.h"
|
||||
#include "foundation/status.h"
|
||||
|
||||
namespace joc::emdf {
|
||||
|
||||
inline constexpr std::uint16_t kSyncword = 0x5838;
|
||||
inline constexpr std::uint8_t kIdOamd = 11;
|
||||
inline constexpr std::uint8_t kIdJoc = 14;
|
||||
inline constexpr std::size_t kMaxPayloads = JOC_MAX_EMDF_PAYLOADS;
|
||||
|
||||
struct Payload {
|
||||
std::uint8_t id = 0;
|
||||
std::uint16_t sample_offset = 0;
|
||||
std::size_t bit_offset = 0; // MSB-first bit position of the payload bytes
|
||||
std::size_t size = 0; // payload byte count
|
||||
};
|
||||
|
||||
struct Container {
|
||||
std::size_t start_bit = 0;
|
||||
std::size_t raw_size = 0;
|
||||
std::size_t payload_count = 0;
|
||||
Payload payloads[kMaxPayloads] = {};
|
||||
|
||||
const Payload* find(std::uint8_t id) const {
|
||||
for (std::size_t i = 0; i < payload_count; ++i) {
|
||||
if (payloads[i].id == id) {
|
||||
return &payloads[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
Status parse_at(const std::uint8_t* data, std::size_t size, std::size_t start_bit, Container* out);
|
||||
|
||||
// All candidate 0x5838 bit offsets over the eight alignments, ascending.
|
||||
void marker_offsets(const std::uint8_t* data, std::size_t size, std::vector<std::size_t>* out);
|
||||
|
||||
Status find_joc_emdf(const std::uint8_t* data, std::size_t size, Container* out);
|
||||
|
||||
// Extract the container's bytes exactly as the bit reader sees them (identical
|
||||
// to a memcpy for byte-aligned containers).
|
||||
void extract_container_bytes(const std::uint8_t* data, std::size_t size, const Container& container,
|
||||
std::vector<std::uint8_t>* out);
|
||||
|
||||
// Extract one payload's bytes with the same MSB-first semantics.
|
||||
Status extract_payload_bytes(const std::uint8_t* data, std::size_t size, const Payload& payload,
|
||||
std::vector<std::uint8_t>* out);
|
||||
|
||||
} // namespace joc::emdf
|
||||
@@ -0,0 +1,30 @@
|
||||
#include "foundation/bit_reader.h"
|
||||
|
||||
namespace joc::bits {
|
||||
|
||||
bool variable_bits(BitReader& reader, unsigned width, unsigned max_groups, std::uint32_t* out_value) {
|
||||
std::uint32_t value = 0;
|
||||
for (unsigned group = 0; group < max_groups; ++group) {
|
||||
value += reader.read(width);
|
||||
if (reader.failed()) {
|
||||
return false;
|
||||
}
|
||||
const std::uint32_t more = reader.read(1);
|
||||
if (reader.failed()) {
|
||||
return false;
|
||||
}
|
||||
if (more == 0u) {
|
||||
if (out_value != nullptr) {
|
||||
*out_value = value;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
value = (value + 1u) << width;
|
||||
}
|
||||
// Same failure mode as the reference implementation: an extension chain
|
||||
// that never terminates is a syntax error, not a truncation.
|
||||
reader.fail(JOC_ERR_EMDF_SYNTAX, "variable_bits extension groups exceeded");
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace joc::bits
|
||||
@@ -0,0 +1,125 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "joc_core.h"
|
||||
|
||||
namespace joc::bits {
|
||||
|
||||
class BitReader {
|
||||
public:
|
||||
BitReader() = default;
|
||||
BitReader(const std::uint8_t* data, std::size_t size) { reset(data, size); }
|
||||
|
||||
void reset(const std::uint8_t* data, std::size_t size, std::size_t start_bit = 0) {
|
||||
data_ = data;
|
||||
size_bits_ = size * 8u;
|
||||
pos_ = start_bit;
|
||||
limit_ = size_bits_;
|
||||
error_ = JOC_OK;
|
||||
message_ = "";
|
||||
}
|
||||
|
||||
void set_limit_bits(std::size_t limit_bits) {
|
||||
limit_ = limit_bits < size_bits_ ? limit_bits : size_bits_;
|
||||
}
|
||||
|
||||
std::size_t position() const { return pos_; }
|
||||
std::size_t limit() const { return limit_; }
|
||||
std::size_t remaining_bits() const { return pos_ <= limit_ ? limit_ - pos_ : 0; }
|
||||
const std::uint8_t* data() const { return data_; }
|
||||
|
||||
bool failed() const { return error_ != JOC_OK; }
|
||||
joc_error error() const { return error_; }
|
||||
const char* error_message() const { return message_; }
|
||||
|
||||
std::uint32_t read(unsigned count) {
|
||||
if (count == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (!can_read(count)) {
|
||||
fail_truncated(count);
|
||||
return 0;
|
||||
}
|
||||
std::uint32_t value = 0;
|
||||
if ((pos_ & 7u) == 0u && count >= 8u) {
|
||||
while (count >= 8u) {
|
||||
value = (value << 8) | data_[pos_ >> 3];
|
||||
pos_ += 8u;
|
||||
count -= 8u;
|
||||
}
|
||||
}
|
||||
while (count-- > 0u) {
|
||||
const std::uint32_t bit = (data_[pos_ >> 3] >> (7u - (pos_ & 7u))) & 1u;
|
||||
value = (value << 1) | bit;
|
||||
++pos_;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
std::uint64_t read64(unsigned count) {
|
||||
if (count <= 32u) {
|
||||
return static_cast<std::uint64_t>(read(count));
|
||||
}
|
||||
const std::uint64_t high = static_cast<std::uint64_t>(read(count - 32u));
|
||||
const std::uint64_t low = static_cast<std::uint64_t>(read(32u));
|
||||
return (high << 32) | low;
|
||||
}
|
||||
|
||||
bool skip(std::size_t count) {
|
||||
if (!can_read(count)) {
|
||||
fail_truncated(count);
|
||||
return false;
|
||||
}
|
||||
pos_ += count;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool read_bytes(std::uint8_t* out, std::size_t count) {
|
||||
if (count == 0) {
|
||||
return true;
|
||||
}
|
||||
if (!can_read(count * 8u)) {
|
||||
fail_truncated(count * 8u);
|
||||
return false;
|
||||
}
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
out[i] = static_cast<std::uint8_t>(read(8u));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool can_read(std::size_t count) const {
|
||||
return !failed() && count <= limit_ && pos_ <= limit_ - count;
|
||||
}
|
||||
|
||||
// semantic check fails, so the reader never continues past it).
|
||||
void fail(joc_error code, const char* message) {
|
||||
if (!failed()) {
|
||||
error_ = code;
|
||||
message_ = message;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void fail_truncated(std::size_t count) {
|
||||
fail(JOC_ERR_BITSTREAM_TRUNCATED, "bit read past end of buffer");
|
||||
last_request_ = count;
|
||||
}
|
||||
|
||||
const std::uint8_t* data_ = nullptr;
|
||||
std::size_t size_bits_ = 0;
|
||||
std::size_t pos_ = 0;
|
||||
std::size_t limit_ = 0;
|
||||
std::size_t last_request_ = 0;
|
||||
joc_error error_ = JOC_OK;
|
||||
const char* message_ = "";
|
||||
};
|
||||
|
||||
// followed by a continuation bit. Mirrors src/emdf.py:variable_bits().
|
||||
bool variable_bits(BitReader& reader, unsigned width, unsigned max_groups,
|
||||
std::uint32_t* out_value);
|
||||
|
||||
} // namespace joc::bits
|
||||
@@ -0,0 +1,206 @@
|
||||
#include "foundation/fft.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "simd/simd.h"
|
||||
|
||||
namespace joc::dsp {
|
||||
|
||||
// The dispatched kernels read and write the spectrum as interleaved doubles, and
|
||||
// an array of std::complex<double> is exactly that: two doubles per element, no
|
||||
// padding, no vtable.
|
||||
static_assert(sizeof(Complex) == 2u * sizeof(double), "complex layout");
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
|
||||
template <typename Container>
|
||||
void fft_in_place(Container* data, bool inverse) {
|
||||
const std::size_t count = data->size();
|
||||
if (count < 2u) {
|
||||
return;
|
||||
}
|
||||
for (std::size_t index = 1u, reversed = 0u; index < count; ++index) {
|
||||
std::size_t bit = count >> 1u;
|
||||
for (; (reversed & bit) != 0u; bit >>= 1u) {
|
||||
reversed ^= bit;
|
||||
}
|
||||
reversed ^= bit;
|
||||
if (index < reversed) {
|
||||
std::swap((*data)[index], (*data)[reversed]);
|
||||
}
|
||||
}
|
||||
for (std::size_t length = 2u; length <= count; length <<= 1u) {
|
||||
const double angle = (inverse ? 2.0 : -2.0) * kPi / static_cast<double>(length);
|
||||
const Complex step(std::cos(angle), std::sin(angle));
|
||||
for (std::size_t start = 0u; start < count; start += length) {
|
||||
Complex factor(1.0, 0.0);
|
||||
for (std::size_t offset = 0u; offset < length / 2u; ++offset) {
|
||||
const Complex even = (*data)[start + offset];
|
||||
const Complex odd = (*data)[start + offset + length / 2u] * factor;
|
||||
(*data)[start + offset] = even + odd;
|
||||
(*data)[start + offset + length / 2u] = even - odd;
|
||||
factor *= step;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inverse) {
|
||||
for (Complex& value : *data) {
|
||||
value /= static_cast<double>(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool is_power_of_two(std::size_t value) { return value != 0u && (value & (value - 1u)) == 0u; }
|
||||
|
||||
} // namespace
|
||||
|
||||
FftPlan::FftPlan(std::size_t size, bool inverse) : size_(size), inverse_(inverse) {
|
||||
reverse_.resize(size);
|
||||
for (std::size_t index = 1u, reversed = 0u; index < size; ++index) {
|
||||
std::size_t bit = size >> 1u;
|
||||
for (; (reversed & bit) != 0u; bit >>= 1u) {
|
||||
reversed ^= bit;
|
||||
}
|
||||
reversed ^= bit;
|
||||
reverse_[index] = reversed;
|
||||
}
|
||||
for (std::size_t length = 2u; length <= size; length <<= 1u) {
|
||||
const double angle = (inverse ? 2.0 : -2.0) * kPi / static_cast<double>(length);
|
||||
const Complex step(std::cos(angle), std::sin(angle));
|
||||
stage_begin_.push_back(twiddle_.size());
|
||||
Complex factor(1.0, 0.0);
|
||||
for (std::size_t offset = 0u; offset < length / 2u; ++offset) {
|
||||
twiddle_.push_back(factor);
|
||||
factor *= step;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exactly the operations fft_in_place performs, in the same order, with the
|
||||
// twiddles read from the precomputed recurrence instead of being re-derived.
|
||||
template <typename Container>
|
||||
void FftPlan::apply(Container* data) const {
|
||||
const std::size_t count = data->size();
|
||||
if (count < 2u) {
|
||||
return;
|
||||
}
|
||||
const std::size_t* reverse = reverse_.data();
|
||||
for (std::size_t index = 1u; index < count; ++index) {
|
||||
const std::size_t reversed = reverse[index];
|
||||
if (index < reversed) {
|
||||
std::swap((*data)[index], (*data)[reversed]);
|
||||
}
|
||||
}
|
||||
// The cascade is dispatched for every power-of-two size the kernels can pack
|
||||
// whole groups into a vector (JOC_SIMD pins one tier for verification). A
|
||||
// kernel only ever puts independent butterflies in the same vector, so every
|
||||
// output keeps the operation sequence and the roundings written below; small
|
||||
// transforms -- and the caller's own table -- keep the portable loop.
|
||||
if (count >= simd::kMinVectorFftSize && (count & (count - 1u)) == 0u) {
|
||||
simd::fft_butterflies(reinterpret_cast<double*>(data->data()), count,
|
||||
reinterpret_cast<const double*>(twiddle_.data()),
|
||||
stage_begin_.data());
|
||||
} else {
|
||||
std::size_t stage = 0u;
|
||||
for (std::size_t length = 2u; length <= count; length <<= 1u, ++stage) {
|
||||
const Complex* table = twiddle_.data() + stage_begin_[stage];
|
||||
for (std::size_t start = 0u; start < count; start += length) {
|
||||
for (std::size_t offset = 0u; offset < length / 2u; ++offset) {
|
||||
const Complex even = (*data)[start + offset];
|
||||
const Complex odd = (*data)[start + offset + length / 2u] * table[offset];
|
||||
(*data)[start + offset] = even + odd;
|
||||
(*data)[start + offset + length / 2u] = even - odd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inverse_) {
|
||||
for (Complex& value : *data) {
|
||||
value /= static_cast<double>(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void fft_radix2(std::vector<Complex>* data, const FftPlan& plan) { plan.apply(data); }
|
||||
|
||||
void fft_radix2(std::array<Complex, kQmfFftSize>* data, const FftPlan& plan) { plan.apply(data); }
|
||||
|
||||
void fft_radix2(std::vector<Complex>* data, bool inverse) { fft_in_place(data, inverse); }
|
||||
|
||||
void fft_radix2(std::array<Complex, kQmfFftSize>* data, bool inverse) {
|
||||
fft_in_place(data, inverse);
|
||||
}
|
||||
|
||||
void fft_any(const std::vector<Complex>& input, bool inverse, std::vector<Complex>* output) {
|
||||
const std::size_t count = input.size();
|
||||
if (is_power_of_two(count)) {
|
||||
*output = input;
|
||||
fft_radix2(output, inverse);
|
||||
return;
|
||||
}
|
||||
std::size_t size = 1u;
|
||||
while (size < 2u * count + 1u) {
|
||||
size <<= 1u;
|
||||
}
|
||||
const double sign = inverse ? 1.0 : -1.0;
|
||||
std::vector<Complex> left(size, Complex(0.0, 0.0));
|
||||
std::vector<Complex> right(size, Complex(0.0, 0.0));
|
||||
for (std::size_t index = 0u; index < count; ++index) {
|
||||
const std::size_t wrapped = (index * index) % (2u * count);
|
||||
const double angle = kPi * static_cast<double>(wrapped) / static_cast<double>(count);
|
||||
const Complex chirp(std::cos(angle), sign * std::sin(angle));
|
||||
left[index] = input[index] * chirp;
|
||||
right[index] = std::conj(chirp);
|
||||
if (index != 0u) {
|
||||
right[size - index] = std::conj(chirp);
|
||||
}
|
||||
}
|
||||
fft_radix2(&left, false);
|
||||
fft_radix2(&right, false);
|
||||
for (std::size_t index = 0u; index < size; ++index) {
|
||||
left[index] *= right[index];
|
||||
}
|
||||
fft_radix2(&left, true);
|
||||
output->resize(count);
|
||||
for (std::size_t index = 0u; index < count; ++index) {
|
||||
const std::size_t wrapped = (index * index) % (2u * count);
|
||||
const double angle = kPi * static_cast<double>(wrapped) / static_cast<double>(count);
|
||||
const Complex chirp(std::cos(angle), sign * std::sin(angle));
|
||||
(*output)[index] = left[index] * chirp;
|
||||
if (inverse) {
|
||||
(*output)[index] /= static_cast<double>(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t next_fast_len(std::size_t value) {
|
||||
if (value <= 6u) {
|
||||
return value;
|
||||
}
|
||||
std::size_t best = value;
|
||||
for (std::size_t power2 = 1u; power2 < value * 2u; power2 *= 2u) {
|
||||
for (std::size_t power3 = power2; power3 < value * 2u; power3 *= 3u) {
|
||||
std::size_t power5 = power3;
|
||||
while (power5 < value) {
|
||||
power5 *= 5u;
|
||||
}
|
||||
best = std::min(best, power5);
|
||||
if (power3 >= value) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
std::size_t next_power_of_two(std::size_t value) {
|
||||
std::size_t result = 1u;
|
||||
while (result < value) {
|
||||
result <<= 1u;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace joc::dsp
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <complex>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
// Complex transforms shared by the HRTF and Rosella DSP cores. The convention is
|
||||
// NumPy's: the forward transform is unnormalised and the inverse scales by 1/N,
|
||||
// so a ported pipeline keeps the reference's arithmetic bit for bit.
|
||||
namespace joc::dsp {
|
||||
|
||||
using Complex = std::complex<double>;
|
||||
|
||||
inline constexpr std::size_t kQmfFftSize = 128;
|
||||
|
||||
// Precomputed radix-2 plan for one size and direction.
|
||||
//
|
||||
// The transform derives each butterfly's twiddle by multiplying the previous one
|
||||
// by the stage step, so the twiddle at offset k is `step` multiplied k times in
|
||||
// that order, independently of the group. Materialising that exact recurrence --
|
||||
// and the bit-reversal permutation -- removes one complex multiply and a
|
||||
// (length/2)-deep serial dependency from every stage's inner loop. The table
|
||||
// entries are the recurrence's own values, so the transform is bit-identical.
|
||||
//
|
||||
// The 128-point cascade is executed by the runtime-dispatched SIMD kernel
|
||||
// (src/simd/simd.h): it computes independent butterflies in parallel lanes,
|
||||
// which leaves both the table and every output's summation order untouched.
|
||||
class FftPlan {
|
||||
public:
|
||||
FftPlan(std::size_t size, bool inverse);
|
||||
|
||||
std::size_t size() const { return size_; }
|
||||
bool inverse() const { return inverse_; }
|
||||
|
||||
private:
|
||||
template <typename Container>
|
||||
void apply(Container* data) const;
|
||||
|
||||
friend void fft_radix2(std::vector<Complex>* data, const FftPlan& plan);
|
||||
friend void fft_radix2(std::array<Complex, kQmfFftSize>* data, const FftPlan& plan);
|
||||
|
||||
std::size_t size_ = 0;
|
||||
bool inverse_ = false;
|
||||
std::vector<std::size_t> reverse_; // bit-reversal permutation, [size]
|
||||
std::vector<std::size_t> stage_begin_; // twiddle offset of each stage
|
||||
std::vector<Complex> twiddle_; // per stage, length/2 entries, concatenated
|
||||
};
|
||||
|
||||
// In-place radix-2 transform; the size must be a power of two.
|
||||
void fft_radix2(std::vector<Complex>* data, bool inverse);
|
||||
void fft_radix2(std::array<Complex, kQmfFftSize>* data, bool inverse);
|
||||
|
||||
// Plan-driven forms: the plan carries the size and the direction, so a caller that
|
||||
// transforms the same length repeatedly builds it once.
|
||||
void fft_radix2(std::vector<Complex>* data, const FftPlan& plan);
|
||||
void fft_radix2(std::array<Complex, kQmfFftSize>* data, const FftPlan& plan);
|
||||
|
||||
// Exact-length transform: radix-2 when the size allows it, Bluestein otherwise.
|
||||
// scipy/numpy use a mixed-radix transform, which is the same transform.
|
||||
void fft_any(const std::vector<Complex>& input, bool inverse, std::vector<Complex>* output);
|
||||
|
||||
// scipy's next_fast_len: the smallest 5-smooth number that is not smaller.
|
||||
std::size_t next_fast_len(std::size_t value);
|
||||
|
||||
std::size_t next_power_of_two(std::size_t value);
|
||||
|
||||
} // namespace joc::dsp
|
||||
@@ -0,0 +1,164 @@
|
||||
#include "foundation/fs_utf8.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#include <shellapi.h>
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
#else
|
||||
#include <cstdlib>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace joc::fs_utf8 {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
fs::path to_path(const std::string& utf8) {
|
||||
return fs::path(std::u8string(reinterpret_cast<const char8_t*>(utf8.data()), utf8.size()));
|
||||
}
|
||||
|
||||
std::string from_path(const fs::path& path) {
|
||||
const std::u8string text = path.u8string();
|
||||
return std::string(reinterpret_cast<const char*>(text.data()), text.size());
|
||||
}
|
||||
|
||||
std::FILE* fopen(const std::string& utf8_path, const char* mode) {
|
||||
#if defined(_WIN32)
|
||||
const std::wstring wide_mode(mode, mode + std::strlen(mode));
|
||||
return ::_wfopen(to_path(utf8_path).c_str(), wide_mode.c_str());
|
||||
#else
|
||||
return std::fopen(utf8_path.c_str(), mode);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::FILE* fopen_spool(const std::string& utf8_path) {
|
||||
#if defined(_WIN32)
|
||||
// Delete-on-close handed to the CRT: if the process is killed the file goes with
|
||||
// it, which is what stops an aborted run from leaving hundreds of gigabytes.
|
||||
HANDLE handle = ::CreateFileW(
|
||||
to_path(utf8_path).c_str(), GENERIC_READ | GENERIC_WRITE | DELETE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, nullptr);
|
||||
if (handle == INVALID_HANDLE_VALUE) {
|
||||
return nullptr;
|
||||
}
|
||||
const int descriptor = ::_open_osfhandle(reinterpret_cast<std::intptr_t>(handle), 0);
|
||||
if (descriptor == -1) {
|
||||
::CloseHandle(handle);
|
||||
return nullptr;
|
||||
}
|
||||
return ::_fdopen(descriptor, "wb+");
|
||||
#else
|
||||
return std::fopen(utf8_path.c_str(), "wb+");
|
||||
#endif
|
||||
}
|
||||
|
||||
int remove(const std::string& utf8_path) {
|
||||
#if defined(_WIN32)
|
||||
return ::_wremove(to_path(utf8_path).c_str());
|
||||
#else
|
||||
return std::remove(utf8_path.c_str());
|
||||
#endif
|
||||
}
|
||||
|
||||
bool exists(const std::string& utf8_path) {
|
||||
std::error_code error;
|
||||
return fs::exists(to_path(utf8_path), error);
|
||||
}
|
||||
|
||||
bool is_directory(const std::string& utf8_path) {
|
||||
std::error_code error;
|
||||
return fs::is_directory(to_path(utf8_path), error);
|
||||
}
|
||||
|
||||
std::uintmax_t file_size(const std::string& utf8_path, std::error_code& error) {
|
||||
return fs::file_size(to_path(utf8_path), error);
|
||||
}
|
||||
|
||||
std::string temp_directory() {
|
||||
std::error_code error;
|
||||
const fs::path directory = fs::temp_directory_path(error);
|
||||
return error ? std::string(".") : from_path(directory);
|
||||
}
|
||||
|
||||
std::string executable_path() {
|
||||
#if defined(_WIN32)
|
||||
std::vector<wchar_t> buffer(MAX_PATH);
|
||||
while (true) {
|
||||
const DWORD written =
|
||||
::GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
|
||||
if (written == 0) {
|
||||
return std::string();
|
||||
}
|
||||
if (written < buffer.size()) {
|
||||
return from_path(fs::path(std::wstring(buffer.data(), written)));
|
||||
}
|
||||
buffer.resize(buffer.size() * 2u);
|
||||
}
|
||||
#elif defined(__linux__)
|
||||
std::vector<char> buffer(4096u, '\0');
|
||||
const ssize_t written = ::readlink("/proc/self/exe", buffer.data(), buffer.size() - 1u);
|
||||
return written > 0 ? std::string(buffer.data(), static_cast<std::size_t>(written))
|
||||
: std::string();
|
||||
#else
|
||||
return std::string();
|
||||
#endif
|
||||
}
|
||||
|
||||
std::ifstream open_input(const std::string& utf8_path) {
|
||||
return std::ifstream(to_path(utf8_path), std::ios::binary);
|
||||
}
|
||||
|
||||
std::ofstream open_output(const std::string& utf8_path) {
|
||||
return std::ofstream(to_path(utf8_path), std::ios::binary);
|
||||
}
|
||||
|
||||
std::vector<std::string> command_line_arguments(int argc, char** argv) {
|
||||
#if defined(_WIN32)
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
int count = 0;
|
||||
LPWSTR* wide = ::CommandLineToArgvW(::GetCommandLineW(), &count);
|
||||
std::vector<std::string> arguments;
|
||||
if (wide == nullptr) {
|
||||
return arguments;
|
||||
}
|
||||
arguments.reserve(static_cast<std::size_t>(count));
|
||||
for (int index = 0; index < count; ++index) {
|
||||
const std::wstring_view text(wide[index]);
|
||||
const int size = ::WideCharToMultiByte(CP_UTF8, 0, text.data(),
|
||||
static_cast<int>(text.size()), nullptr, 0, nullptr,
|
||||
nullptr);
|
||||
std::string utf8(static_cast<std::size_t>(size), '\0');
|
||||
if (size > 0) {
|
||||
::WideCharToMultiByte(CP_UTF8, 0, text.data(), static_cast<int>(text.size()),
|
||||
utf8.data(), size, nullptr, nullptr);
|
||||
}
|
||||
arguments.push_back(std::move(utf8));
|
||||
}
|
||||
::LocalFree(wide);
|
||||
return arguments;
|
||||
#else
|
||||
std::vector<std::string> arguments;
|
||||
arguments.reserve(static_cast<std::size_t>(argc));
|
||||
for (int index = 0; index < argc; ++index) {
|
||||
arguments.emplace_back(argv[index]);
|
||||
}
|
||||
return arguments;
|
||||
#endif
|
||||
}
|
||||
|
||||
void configure_console() {
|
||||
#if defined(_WIN32)
|
||||
::SetConsoleOutputCP(CP_UTF8);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace joc::fs_utf8
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
// Paths inside this library are always UTF-8, on every platform. std::filesystem
|
||||
// stores UTF-16 on Windows and bytes elsewhere, and the narrow CRT uses the ANSI
|
||||
// code page on Windows, so every path crosses into the OS through this shim: that
|
||||
// is what makes non-ASCII names (Japanese, Chinese, ...) work.
|
||||
namespace joc::fs_utf8 {
|
||||
|
||||
std::filesystem::path to_path(const std::string& utf8);
|
||||
std::string from_path(const std::filesystem::path& path);
|
||||
|
||||
// File handles and queries take a UTF-8 path: _wfopen on Windows, plain calls
|
||||
// elsewhere. Nothing else in the library may call the narrow CRT with a path.
|
||||
std::FILE* fopen(const std::string& utf8_path, const char* mode);
|
||||
// Temporary spool handle: on Windows the file is opened delete-on-close, so killing
|
||||
// the process removes it instead of leaving a multi-gigabyte leftover behind.
|
||||
std::FILE* fopen_spool(const std::string& utf8_path);
|
||||
int remove(const std::string& utf8_path);
|
||||
bool exists(const std::string& utf8_path);
|
||||
bool is_directory(const std::string& utf8_path);
|
||||
std::uintmax_t file_size(const std::string& utf8_path, std::error_code& error);
|
||||
std::string temp_directory();
|
||||
|
||||
// The running executable's own path, UTF-8, or empty when the platform cannot
|
||||
// report it. Defaults are anchored here so they never depend on the CWD.
|
||||
std::string executable_path();
|
||||
|
||||
// Streams: std::ifstream/ofstream accept a std::filesystem::path, which is the
|
||||
// portable way to open a UTF-8 path.
|
||||
std::ifstream open_input(const std::string& utf8_path);
|
||||
std::ofstream open_output(const std::string& utf8_path);
|
||||
|
||||
// Command line arguments as UTF-8. Windows hands the process UTF-16 and the
|
||||
// narrow CRT would convert it through the ANSI code page, so the wide command
|
||||
// line is re-parsed there; on POSIX argv is already bytes in the user's locale.
|
||||
std::vector<std::string> command_line_arguments(int argc, char** argv);
|
||||
void configure_console();
|
||||
|
||||
} // namespace joc::fs_utf8
|
||||
@@ -0,0 +1,25 @@
|
||||
// Port of the reference's adm_atmos.q_to_adm_xyz: OAMD Q15 coordinates to the ADM
|
||||
// cartesian triple. It lives in foundation because both the ADM writer and the
|
||||
// object position timeline need it, and the timeline must not depend on output.
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "foundation/py_num.h"
|
||||
|
||||
namespace joc::geometry {
|
||||
|
||||
inline void q_to_adm_xyz(int q1, int q2, int q3, double* x, double* y, double* z) {
|
||||
const double posX = std::min(1.0, static_cast<double>(pynum::py_round(
|
||||
static_cast<double>(q1) * 62.0 / 32767.0)) / 62.0);
|
||||
const double posY = std::min(1.0, static_cast<double>(pynum::py_round(
|
||||
static_cast<double>(q2) * 62.0 / 32767.0)) / 62.0);
|
||||
double posZ = static_cast<double>(pynum::py_round(
|
||||
static_cast<double>(q3) * 15.0 / 32767.0)) / 15.0;
|
||||
posZ = std::max(-1.0, std::min(1.0, posZ));
|
||||
*x = posX * 2.0 - 1.0;
|
||||
*y = 1.0 - posY * 2.0;
|
||||
*z = posZ;
|
||||
}
|
||||
|
||||
} // namespace joc::geometry
|
||||
@@ -0,0 +1,227 @@
|
||||
#include "foundation/mini_json.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace joc::json {
|
||||
|
||||
namespace {
|
||||
|
||||
void skip_space(const std::string& text, std::size_t* index) {
|
||||
while (*index < text.size() &&
|
||||
(text[*index] == ' ' || text[*index] == '\t' || text[*index] == '\n' ||
|
||||
text[*index] == '\r')) {
|
||||
++(*index);
|
||||
}
|
||||
}
|
||||
|
||||
bool read_string(const std::string& text, std::size_t* index, std::string* out) {
|
||||
if (*index >= text.size() || text[*index] != '"') {
|
||||
return false;
|
||||
}
|
||||
++(*index);
|
||||
out->clear();
|
||||
while (*index < text.size()) {
|
||||
const char c = text[*index];
|
||||
if (c == '\\') {
|
||||
if (*index + 1 >= text.size()) {
|
||||
return false;
|
||||
}
|
||||
const char escape = text[*index + 1];
|
||||
*index += 2;
|
||||
switch (escape) {
|
||||
case '"': out->push_back('"'); break;
|
||||
case '\\': out->push_back('\\'); break;
|
||||
case '/': out->push_back('/'); break;
|
||||
case 'b': out->push_back('\b'); break;
|
||||
case 'f': out->push_back('\f'); break;
|
||||
case 'n': out->push_back('\n'); break;
|
||||
case 'r': out->push_back('\r'); break;
|
||||
case 't': out->push_back('\t'); break;
|
||||
case 'u': {
|
||||
if (*index + 4 > text.size()) {
|
||||
return false;
|
||||
}
|
||||
unsigned code = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const char digit = text[*index + static_cast<std::size_t>(i)];
|
||||
code <<= 4;
|
||||
if (digit >= '0' && digit <= '9') { code |= static_cast<unsigned>(digit - '0'); }
|
||||
else if (digit >= 'a' && digit <= 'f') { code |= static_cast<unsigned>(digit - 'a' + 10); }
|
||||
else if (digit >= 'A' && digit <= 'F') { code |= static_cast<unsigned>(digit - 'A' + 10); }
|
||||
else { return false; }
|
||||
}
|
||||
*index += 4;
|
||||
if (code < 0x80u) {
|
||||
out->push_back(static_cast<char>(code));
|
||||
} else if (code < 0x800u) {
|
||||
out->push_back(static_cast<char>(0xC0u | (code >> 6)));
|
||||
out->push_back(static_cast<char>(0x80u | (code & 0x3Fu)));
|
||||
} else {
|
||||
out->push_back(static_cast<char>(0xE0u | (code >> 12)));
|
||||
out->push_back(static_cast<char>(0x80u | ((code >> 6) & 0x3Fu)));
|
||||
out->push_back(static_cast<char>(0x80u | (code & 0x3Fu)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == '"') {
|
||||
++(*index);
|
||||
return true;
|
||||
}
|
||||
out->push_back(c);
|
||||
++(*index);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool read_compound(const std::string& text, std::size_t* index, std::string* out) {
|
||||
const char open = text[*index];
|
||||
const char close = open == '{' ? '}' : ']';
|
||||
int depth = 0;
|
||||
const std::size_t start = *index;
|
||||
while (*index < text.size()) {
|
||||
const char c = text[*index];
|
||||
if (c == '"') {
|
||||
std::string ignored;
|
||||
if (!read_string(text, index, &ignored)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == open) {
|
||||
++depth;
|
||||
} else if (c == close) {
|
||||
--depth;
|
||||
if (depth == 0) {
|
||||
++(*index);
|
||||
*out = text.substr(start, *index - start);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
++(*index);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool parse_object(const std::string& text, std::vector<Member>* out, std::string* error) {
|
||||
out->clear();
|
||||
std::size_t index = 0;
|
||||
skip_space(text, &index);
|
||||
if (index >= text.size() || text[index] != '{') {
|
||||
if (error != nullptr) { *error = "metadata is not a JSON object"; }
|
||||
return false;
|
||||
}
|
||||
++index;
|
||||
for (;;) {
|
||||
skip_space(text, &index);
|
||||
if (index < text.size() && text[index] == '}') {
|
||||
++index;
|
||||
break;
|
||||
}
|
||||
if (index >= text.size() || text[index] == ',') {
|
||||
if (index >= text.size()) {
|
||||
if (error != nullptr) { *error = "unterminated JSON object"; }
|
||||
return false;
|
||||
}
|
||||
++index;
|
||||
continue;
|
||||
}
|
||||
Member member;
|
||||
if (!read_string(text, &index, &member.key)) {
|
||||
if (error != nullptr) { *error = "expected a JSON key"; }
|
||||
return false;
|
||||
}
|
||||
skip_space(text, &index);
|
||||
if (index >= text.size() || text[index] != ':') {
|
||||
if (error != nullptr) { *error = "expected ':' after JSON key " + member.key; }
|
||||
return false;
|
||||
}
|
||||
++index;
|
||||
skip_space(text, &index);
|
||||
if (index >= text.size()) {
|
||||
if (error != nullptr) { *error = "missing JSON value for " + member.key; }
|
||||
return false;
|
||||
}
|
||||
if (text[index] == '"') {
|
||||
member.is_string = true;
|
||||
if (!read_string(text, &index, &member.raw)) {
|
||||
if (error != nullptr) { *error = "bad JSON string for " + member.key; }
|
||||
return false;
|
||||
}
|
||||
} else if (text[index] == '{' || text[index] == '[') {
|
||||
if (!read_compound(text, &index, &member.raw)) {
|
||||
if (error != nullptr) { *error = "bad JSON container for " + member.key; }
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const std::size_t start = index;
|
||||
while (index < text.size() && text[index] != ',' && text[index] != '}') {
|
||||
++index;
|
||||
}
|
||||
member.raw = text.substr(start, index - start);
|
||||
while (!member.raw.empty() &&
|
||||
(member.raw.back() == ' ' || member.raw.back() == '\n' ||
|
||||
member.raw.back() == '\r' || member.raw.back() == '\t')) {
|
||||
member.raw.pop_back();
|
||||
}
|
||||
}
|
||||
for (const Member& existing : *out) {
|
||||
if (existing.key == member.key) {
|
||||
if (error != nullptr) { *error = "duplicate JSON key " + member.key; }
|
||||
return false;
|
||||
}
|
||||
}
|
||||
out->push_back(std::move(member));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const Member* find(const std::vector<Member>& members, const std::string& key) {
|
||||
for (const Member& member : members) {
|
||||
if (member.key == key) {
|
||||
return &member;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool as_string(const Member& member, std::string* out) {
|
||||
if (!member.is_string || out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
*out = member.raw;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool as_number(const Member& member, double* out) {
|
||||
if (member.is_string || out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
char* end = nullptr;
|
||||
const double value = std::strtod(member.raw.c_str(), &end);
|
||||
if (end == member.raw.c_str() || !std::isfinite(value)) {
|
||||
return false;
|
||||
}
|
||||
*out = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool as_integer(const Member& member, long long* out) {
|
||||
double value = 0.0;
|
||||
if (!as_number(member, &value) || out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (value != std::floor(value)) {
|
||||
return false;
|
||||
}
|
||||
*out = static_cast<long long>(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace joc::json
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace joc::json {
|
||||
|
||||
struct Member {
|
||||
std::string key;
|
||||
std::string raw;
|
||||
bool is_string = false;
|
||||
};
|
||||
|
||||
// Parses a top-level JSON object. Rejects non-objects and duplicate keys.
|
||||
bool parse_object(const std::string& text, std::vector<Member>* out, std::string* error);
|
||||
|
||||
const Member* find(const std::vector<Member>& members, const std::string& key);
|
||||
|
||||
bool as_string(const Member& member, std::string* out);
|
||||
bool as_number(const Member& member, double* out);
|
||||
bool as_integer(const Member& member, long long* out);
|
||||
|
||||
} // namespace joc::json
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cfenv>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
namespace joc::pynum {
|
||||
|
||||
inline long long py_round(double value) {
|
||||
return static_cast<long long>(std::nearbyint(value));
|
||||
}
|
||||
|
||||
inline std::string format_fixed(double value, int decimals) {
|
||||
char buffer[64];
|
||||
std::snprintf(buffer, sizeof(buffer), "%.*f", decimals, value);
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
inline long long trunc_to_ll(double value) {
|
||||
return static_cast<long long>(value);
|
||||
}
|
||||
|
||||
} // namespace joc::pynum
|
||||
@@ -0,0 +1,165 @@
|
||||
#include "foundation/sha256.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace joc::crypto {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t kK[64] = {
|
||||
0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu, 0x59f111f1u, 0x923f82a4u,
|
||||
0xab1c5ed5u, 0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u, 0x72be5d74u, 0x80deb1feu,
|
||||
0x9bdc06a7u, 0xc19bf174u, 0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, 0x2de92c6fu,
|
||||
0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau, 0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u,
|
||||
0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u, 0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu,
|
||||
0x53380d13u, 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u, 0xa2bfe8a1u, 0xa81a664bu,
|
||||
0xc24b8b70u, 0xc76c51a3u, 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u, 0x19a4c116u,
|
||||
0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u,
|
||||
0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, 0x90befffau, 0xa4506cebu, 0xbef9a3f7u,
|
||||
0xc67178f2u};
|
||||
|
||||
inline std::uint32_t rotr(std::uint32_t value, unsigned count) {
|
||||
return (value >> count) | (value << (32u - count));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Sha256::reset() {
|
||||
state_[0] = 0x6a09e667u;
|
||||
state_[1] = 0xbb67ae85u;
|
||||
state_[2] = 0x3c6ef372u;
|
||||
state_[3] = 0xa54ff53au;
|
||||
state_[4] = 0x510e527fu;
|
||||
state_[5] = 0x9b05688cu;
|
||||
state_[6] = 0x1f83d9abu;
|
||||
state_[7] = 0x5be0cd19u;
|
||||
bit_count_ = 0;
|
||||
buffer_used_ = 0;
|
||||
std::memset(buffer_, 0, sizeof(buffer_));
|
||||
}
|
||||
|
||||
void Sha256::transform(const std::uint8_t block[64]) {
|
||||
std::uint32_t w[64];
|
||||
for (unsigned i = 0; i < 16; ++i) {
|
||||
w[i] = (static_cast<std::uint32_t>(block[i * 4]) << 24) |
|
||||
(static_cast<std::uint32_t>(block[i * 4 + 1]) << 16) |
|
||||
(static_cast<std::uint32_t>(block[i * 4 + 2]) << 8) |
|
||||
static_cast<std::uint32_t>(block[i * 4 + 3]);
|
||||
}
|
||||
for (unsigned i = 16; i < 64; ++i) {
|
||||
const std::uint32_t s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3);
|
||||
const std::uint32_t s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10);
|
||||
w[i] = w[i - 16] + s0 + w[i - 7] + s1;
|
||||
}
|
||||
std::uint32_t a = state_[0];
|
||||
std::uint32_t b = state_[1];
|
||||
std::uint32_t c = state_[2];
|
||||
std::uint32_t d = state_[3];
|
||||
std::uint32_t e = state_[4];
|
||||
std::uint32_t f = state_[5];
|
||||
std::uint32_t g = state_[6];
|
||||
std::uint32_t h = state_[7];
|
||||
for (unsigned i = 0; i < 64; ++i) {
|
||||
const std::uint32_t s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
||||
const std::uint32_t ch = (e & f) ^ (~e & g);
|
||||
const std::uint32_t temp1 = h + s1 + ch + kK[i] + w[i];
|
||||
const std::uint32_t s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
||||
const std::uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
|
||||
const std::uint32_t temp2 = s0 + maj;
|
||||
h = g;
|
||||
g = f;
|
||||
f = e;
|
||||
e = d + temp1;
|
||||
d = c;
|
||||
c = b;
|
||||
b = a;
|
||||
a = temp1 + temp2;
|
||||
}
|
||||
state_[0] += a;
|
||||
state_[1] += b;
|
||||
state_[2] += c;
|
||||
state_[3] += d;
|
||||
state_[4] += e;
|
||||
state_[5] += f;
|
||||
state_[6] += g;
|
||||
state_[7] += h;
|
||||
}
|
||||
|
||||
void Sha256::update(const void* data, std::size_t size) {
|
||||
const std::uint8_t* bytes = static_cast<const std::uint8_t*>(data);
|
||||
bit_count_ += static_cast<std::uint64_t>(size) * 8u;
|
||||
while (size > 0) {
|
||||
const std::size_t space = 64u - buffer_used_;
|
||||
const std::size_t take = size < space ? size : space;
|
||||
std::memcpy(buffer_ + buffer_used_, bytes, take);
|
||||
buffer_used_ += take;
|
||||
bytes += take;
|
||||
size -= take;
|
||||
if (buffer_used_ == 64u) {
|
||||
transform(buffer_);
|
||||
buffer_used_ = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Sha256::finish(std::uint8_t out[32]) {
|
||||
const std::uint64_t total_bits = bit_count_;
|
||||
const std::uint8_t pad = 0x80u;
|
||||
update(&pad, 1);
|
||||
const std::uint8_t zero = 0x00u;
|
||||
while (buffer_used_ != 56u) {
|
||||
update(&zero, 1);
|
||||
}
|
||||
std::uint8_t length_bytes[8];
|
||||
for (unsigned i = 0; i < 8; ++i) {
|
||||
length_bytes[i] = static_cast<std::uint8_t>((total_bits >> (56u - i * 8u)) & 0xFFu);
|
||||
}
|
||||
std::memcpy(buffer_ + buffer_used_, length_bytes, 8);
|
||||
buffer_used_ += 8;
|
||||
transform(buffer_);
|
||||
buffer_used_ = 0;
|
||||
for (unsigned i = 0; i < 8; ++i) {
|
||||
out[i * 4 + 0] = static_cast<std::uint8_t>((state_[i] >> 24) & 0xFFu);
|
||||
out[i * 4 + 1] = static_cast<std::uint8_t>((state_[i] >> 16) & 0xFFu);
|
||||
out[i * 4 + 2] = static_cast<std::uint8_t>((state_[i] >> 8) & 0xFFu);
|
||||
out[i * 4 + 3] = static_cast<std::uint8_t>(state_[i] & 0xFFu);
|
||||
}
|
||||
}
|
||||
|
||||
std::string Sha256::finish_hex() {
|
||||
std::uint8_t digest[32];
|
||||
finish(digest);
|
||||
static const char* kHex = "0123456789abcdef";
|
||||
std::string text;
|
||||
text.resize(64);
|
||||
for (unsigned i = 0; i < 32; ++i) {
|
||||
text[i * 2] = kHex[(digest[i] >> 4) & 0x0Fu];
|
||||
text[i * 2 + 1] = kHex[digest[i] & 0x0Fu];
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string sha256_hex(const void* data, std::size_t size) {
|
||||
Sha256 hash;
|
||||
hash.update(data, size);
|
||||
return hash.finish_hex();
|
||||
}
|
||||
|
||||
bool sha256_hex_matches(const void* data, std::size_t size, const std::string& expected_hex) {
|
||||
if (expected_hex.size() != 64) {
|
||||
return false;
|
||||
}
|
||||
std::string actual = sha256_hex(data, size);
|
||||
for (std::size_t i = 0; i < 64; ++i) {
|
||||
char expected = expected_hex[i];
|
||||
if (expected >= 'A' && expected <= 'F') {
|
||||
expected = static_cast<char>(expected - 'A' + 'a');
|
||||
}
|
||||
if (actual[i] != expected) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace joc::crypto
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace joc::crypto {
|
||||
|
||||
class Sha256 {
|
||||
public:
|
||||
Sha256() { reset(); }
|
||||
|
||||
void reset();
|
||||
void update(const void* data, std::size_t size);
|
||||
void finish(std::uint8_t out[32]);
|
||||
std::string finish_hex();
|
||||
|
||||
private:
|
||||
void transform(const std::uint8_t block[64]);
|
||||
|
||||
std::uint32_t state_[8] = {};
|
||||
std::uint64_t bit_count_ = 0;
|
||||
std::uint8_t buffer_[64] = {};
|
||||
std::size_t buffer_used_ = 0;
|
||||
};
|
||||
|
||||
std::string sha256_hex(const void* data, std::size_t size);
|
||||
bool sha256_hex_matches(const void* data, std::size_t size, const std::string& expected_hex);
|
||||
|
||||
} // namespace joc::crypto
|
||||
@@ -0,0 +1,48 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "joc_core.h"
|
||||
|
||||
namespace joc {
|
||||
|
||||
class Status {
|
||||
public:
|
||||
Status() = default;
|
||||
|
||||
static Status success() { return Status(); }
|
||||
|
||||
static Status fail(joc_error code, std::string stage, std::string message) {
|
||||
Status s;
|
||||
s.code_ = code;
|
||||
s.stage_ = std::move(stage);
|
||||
s.message_ = std::move(message);
|
||||
return s;
|
||||
}
|
||||
|
||||
bool ok() const { return code_ == JOC_OK; }
|
||||
joc_error code() const { return code_; }
|
||||
const std::string& stage() const { return stage_; }
|
||||
const std::string& message() const { return message_; }
|
||||
|
||||
private:
|
||||
joc_error code_ = JOC_OK;
|
||||
std::string stage_ = "none";
|
||||
std::string message_;
|
||||
};
|
||||
|
||||
// Stage names are kept as plain literals so that C++ and the Python frontend
|
||||
namespace stage {
|
||||
inline constexpr const char* kFoundation = "foundation";
|
||||
inline constexpr const char* kEac3 = "eac3_transport";
|
||||
inline constexpr const char* kEmdf = "emdf";
|
||||
inline constexpr const char* kJoc = "joc";
|
||||
inline constexpr const char* kOamd = "oamd";
|
||||
inline constexpr const char* kDsp = "dsp";
|
||||
inline constexpr const char* kRender = "render";
|
||||
inline constexpr const char* kOutput = "output";
|
||||
} // namespace stage
|
||||
|
||||
} // namespace joc
|
||||
@@ -0,0 +1,401 @@
|
||||
#include "hrtf/jochrtf.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "foundation/mini_json.h"
|
||||
#include "foundation/sha256.h"
|
||||
#include "io/npy.h"
|
||||
#include "io/zip_reader.h"
|
||||
|
||||
namespace joc::hrtf {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string to_upper(std::string text) {
|
||||
for (char& c : text) {
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
c = static_cast<char>(c - 'a' + 'A');
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
bool is_sha256_hex(const std::string& text) {
|
||||
if (text.size() != 64) {
|
||||
return false;
|
||||
}
|
||||
for (const char c : text) {
|
||||
const bool digit = c >= '0' && c <= '9';
|
||||
const bool upper = c >= 'A' && c <= 'F';
|
||||
if (!digit && !upper) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// json.dumps(list(shape)) as the reference writes it, e.g. "[36, 2, 77]".
|
||||
std::string shape_json(const std::vector<std::int64_t>& shape) {
|
||||
std::string text = "[";
|
||||
for (std::size_t i = 0; i < shape.size(); ++i) {
|
||||
text += (i == 0 ? "" : ", ");
|
||||
text += std::to_string(shape[i]);
|
||||
}
|
||||
text += "]";
|
||||
return text;
|
||||
}
|
||||
|
||||
Status hrtf_fail(const std::string& message) {
|
||||
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender, message);
|
||||
}
|
||||
|
||||
std::string payload_sha256(const std::vector<double>& centers,
|
||||
const std::vector<double>& coefficients,
|
||||
const std::vector<double>& delay_coefficients,
|
||||
const std::vector<double>& delay_bounds) {
|
||||
crypto::Sha256 hash;
|
||||
const char prefix[] = "JOC-HRTF-CACHE-PAYLOAD-V1";
|
||||
hash.update(prefix, sizeof(prefix) - 1);
|
||||
const std::uint8_t zero = 0;
|
||||
hash.update(&zero, 1);
|
||||
|
||||
struct Entry {
|
||||
const char* name;
|
||||
const char* dtype;
|
||||
const std::vector<double>* values;
|
||||
std::vector<std::int64_t> shape;
|
||||
};
|
||||
const Entry entries[4] = {
|
||||
{"band_center_frequencies_hz", "<f8", ¢ers, {kHybridBands}},
|
||||
{"coefficients", "<c16", &coefficients, {kShTerms, kEars, kHybridBands}},
|
||||
{"delay_coefficients", "<f8", &delay_coefficients, {kShTerms, kEars}},
|
||||
{"delay_bounds", "<f8", &delay_bounds, {2, 2}},
|
||||
};
|
||||
for (const Entry& entry : entries) {
|
||||
const std::string name(entry.name);
|
||||
const std::string dtype(entry.dtype);
|
||||
const std::string shape = shape_json(entry.shape);
|
||||
hash.update(name.data(), name.size());
|
||||
hash.update(&zero, 1);
|
||||
hash.update(dtype.data(), dtype.size());
|
||||
hash.update(&zero, 1);
|
||||
hash.update(shape.data(), shape.size());
|
||||
hash.update(&zero, 1);
|
||||
hash.update(entry.values->data(), entry.values->size() * sizeof(double));
|
||||
}
|
||||
return hash.finish_hex();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status load_jochrtf(const std::string& path, Field* out) {
|
||||
if (out == nullptr) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kRender, "null field");
|
||||
}
|
||||
io::ZipArchive archive;
|
||||
std::string error;
|
||||
if (!archive.open(path, &error)) {
|
||||
return Status::fail(JOC_ERR_HRTF_NOT_FOUND, stage::kRender,
|
||||
"cannot read compiled HRTF " + path + ": " + error);
|
||||
}
|
||||
|
||||
// Member set must be exactly the five expected names.
|
||||
static const char* kMembers[5] = {"metadata_json.npy", "band_center_frequencies_hz.npy",
|
||||
"coefficients.npy", "delay_coefficients.npy",
|
||||
"delay_bounds.npy"};
|
||||
if (archive.entries().size() != 5u) {
|
||||
return hrtf_fail("compiled HRTF cache has an invalid member set (" +
|
||||
std::to_string(archive.entries().size()) + " members)");
|
||||
}
|
||||
for (const char* name : kMembers) {
|
||||
if (archive.find(name) == nullptr) {
|
||||
return hrtf_fail(std::string("compiled HRTF cache is missing ") + name);
|
||||
}
|
||||
}
|
||||
|
||||
auto read_member = [&](const char* name, std::vector<std::uint8_t>* raw,
|
||||
io::NpyArray* array) -> Status {
|
||||
if (!archive.read_member(name, raw, &error)) {
|
||||
return hrtf_fail(std::string("compiled HRTF member ") + name + ": " + error);
|
||||
}
|
||||
if (!io::parse_npy(raw->data(), raw->size(), array, &error)) {
|
||||
return hrtf_fail(std::string("compiled HRTF member ") + name + ": " + error);
|
||||
}
|
||||
if (array->fortran_order) {
|
||||
return hrtf_fail(std::string("compiled HRTF member must be C-contiguous: ") + name);
|
||||
}
|
||||
return Status::success();
|
||||
};
|
||||
|
||||
std::vector<std::uint8_t> raw;
|
||||
io::NpyArray array;
|
||||
|
||||
Status status = read_member("metadata_json.npy", &raw, &array);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
std::string metadata_text;
|
||||
if (!io::npy_unicode_to_utf8(array, &metadata_text, &error)) {
|
||||
return hrtf_fail("compiled HRTF metadata: " + error);
|
||||
}
|
||||
if (metadata_text.size() > 64u * 1024u) {
|
||||
return hrtf_fail("compiled HRTF metadata is too large");
|
||||
}
|
||||
|
||||
status = read_member("band_center_frequencies_hz.npy", &raw, &array);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (array.descr != "<f8" || !io::npy_shape_is(array, {kHybridBands})) {
|
||||
return hrtf_fail("band_center_frequencies_hz must be <f8(77,)");
|
||||
}
|
||||
std::vector<double> centers;
|
||||
io::npy_to_double(array, ¢ers, &error);
|
||||
|
||||
status = read_member("coefficients.npy", &raw, &array);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (array.descr != "<c16" || !io::npy_shape_is(array, {kShTerms, kEars, kHybridBands})) {
|
||||
return hrtf_fail("coefficients must be <c16(36, 2, 77)");
|
||||
}
|
||||
std::vector<double> coefficients;
|
||||
if (!io::npy_to_double(array, &coefficients, &error)) {
|
||||
return hrtf_fail("coefficients: " + error);
|
||||
}
|
||||
|
||||
status = read_member("delay_coefficients.npy", &raw, &array);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (array.descr != "<f8" || !io::npy_shape_is(array, {kShTerms, kEars})) {
|
||||
return hrtf_fail("delay_coefficients must be <f8(36, 2)");
|
||||
}
|
||||
std::vector<double> delay_coefficients;
|
||||
io::npy_to_double(array, &delay_coefficients, &error);
|
||||
|
||||
status = read_member("delay_bounds.npy", &raw, &array);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (array.descr != "<f8" || !io::npy_shape_is(array, {2, 2})) {
|
||||
return hrtf_fail("delay_bounds must be <f8(2, 2)");
|
||||
}
|
||||
std::vector<double> delay_bounds;
|
||||
io::npy_to_double(array, &delay_bounds, &error);
|
||||
|
||||
std::vector<json::Member> members;
|
||||
if (!json::parse_object(metadata_text, &members, &error)) {
|
||||
return hrtf_fail("compiled HRTF metadata: " + error);
|
||||
}
|
||||
auto require_string = [&](const char* key, std::string* value) -> Status {
|
||||
const json::Member* member = json::find(members, key);
|
||||
if (member == nullptr || !json::as_string(*member, value)) {
|
||||
return hrtf_fail(std::string("compiled HRTF metadata is missing ") + key);
|
||||
}
|
||||
return Status::success();
|
||||
};
|
||||
std::string magic;
|
||||
std::string schema;
|
||||
std::string source_sha256;
|
||||
std::string cache_key;
|
||||
std::string payload_hash;
|
||||
std::string delay_source;
|
||||
status = require_string("magic", &magic);
|
||||
if (!status.ok()) { return status; }
|
||||
status = require_string("cache_schema", &schema);
|
||||
if (!status.ok()) { return status; }
|
||||
status = require_string("source_sha256", &source_sha256);
|
||||
if (!status.ok()) { return status; }
|
||||
status = require_string("cache_key", &cache_key);
|
||||
if (!status.ok()) { return status; }
|
||||
status = require_string("payload_sha256", &payload_hash);
|
||||
if (!status.ok()) { return status; }
|
||||
status = require_string("delay_source", &delay_source);
|
||||
if (!status.ok()) { return status; }
|
||||
|
||||
if (magic != kMagic) {
|
||||
return hrtf_fail("compiled HRTF magic mismatch: " + magic);
|
||||
}
|
||||
if (schema != kCacheSchema) {
|
||||
return hrtf_fail("compiled HRTF cache schema mismatch: " + schema);
|
||||
}
|
||||
const json::Member* version_member = json::find(members, "format_version");
|
||||
long long version = -1;
|
||||
if (version_member == nullptr || !json::as_integer(*version_member, &version)) {
|
||||
return hrtf_fail("compiled HRTF metadata is missing format_version");
|
||||
}
|
||||
if (version != kFormatVersion) {
|
||||
return Status::fail(JOC_ERR_HRTF_VERSION, stage::kRender,
|
||||
"unsupported .jochrtf version " + std::to_string(version) +
|
||||
"; rebuild it from the source SOFA");
|
||||
}
|
||||
out->source_sha256 = to_upper(source_sha256);
|
||||
out->cache_key = to_upper(cache_key);
|
||||
if (!is_sha256_hex(out->source_sha256)) {
|
||||
return hrtf_fail("compiled HRTF source_sha256 is not a 64-digit digest");
|
||||
}
|
||||
if (!is_sha256_hex(out->cache_key)) {
|
||||
return hrtf_fail("compiled HRTF cache_key is not a 64-digit digest");
|
||||
}
|
||||
|
||||
const std::string expected = payload_sha256(centers, coefficients, delay_coefficients,
|
||||
delay_bounds);
|
||||
if (to_upper(payload_hash) != to_upper(expected)) {
|
||||
return Status::fail(JOC_ERR_HRTF_HASH, stage::kRender,
|
||||
"compiled HRTF payload hash mismatch");
|
||||
}
|
||||
out->payload_sha256 = to_upper(payload_hash);
|
||||
|
||||
const json::Member* radius_member = json::find(members, "measurement_radius_m");
|
||||
double radius = 0.0;
|
||||
if (radius_member == nullptr || !json::as_number(*radius_member, &radius) || radius <= 0.0) {
|
||||
return hrtf_fail("compiled HRTF measurement_radius_m must be a positive number");
|
||||
}
|
||||
out->measurement_radius_m = radius;
|
||||
const json::Member* order_member = json::find(members, "order");
|
||||
long long order = 0;
|
||||
if (order_member == nullptr || !json::as_integer(*order_member, &order) || order <= 0 ||
|
||||
order * order > kShTerms) {
|
||||
return hrtf_fail("compiled HRTF order is out of range");
|
||||
}
|
||||
out->order = order;
|
||||
|
||||
for (const double value : coefficients) {
|
||||
if (!std::isfinite(value)) {
|
||||
return hrtf_fail("compiled HRTF coefficients contain non-finite values");
|
||||
}
|
||||
}
|
||||
for (const double value : delay_coefficients) {
|
||||
if (!std::isfinite(value) || std::abs(value) > 48000.0 * 64.0) {
|
||||
return hrtf_fail("compiled HRTF delay coefficients are out of range");
|
||||
}
|
||||
}
|
||||
for (const double value : delay_bounds) {
|
||||
if (!std::isfinite(value)) {
|
||||
return hrtf_fail("compiled HRTF delay bounds contain non-finite values");
|
||||
}
|
||||
}
|
||||
if (delay_bounds.size() == 4u && delay_bounds[0] > delay_bounds[1]) {
|
||||
return hrtf_fail("compiled HRTF delay bounds are inverted");
|
||||
}
|
||||
|
||||
if (const json::Member* member = json::find(members, "compiler_version")) {
|
||||
json::as_string(*member, &out->compiler_version);
|
||||
}
|
||||
if (const json::Member* member = json::find(members, "phase_policy_version")) {
|
||||
json::as_string(*member, &out->phase_policy_version);
|
||||
}
|
||||
if (const json::Member* member = json::find(members, "sh_convention")) {
|
||||
json::as_string(*member, &out->sh_convention);
|
||||
}
|
||||
if (const json::Member* member = json::find(members, "source_display_name")) {
|
||||
json::as_string(*member, &out->source_display_name);
|
||||
}
|
||||
if (const json::Member* member = json::find(members, "projection_ridge")) {
|
||||
json::as_number(*member, &out->projection_ridge);
|
||||
}
|
||||
if (const json::Member* member = json::find(members, "spherical_harmonic_ridge")) {
|
||||
json::as_number(*member, &out->spherical_harmonic_ridge);
|
||||
}
|
||||
if (const json::Member* member = json::find(members, "fit_report")) {
|
||||
out->fit_report_json = member->raw;
|
||||
}
|
||||
if (const json::Member* member = json::find(members, "filterbank")) {
|
||||
out->filterbank_json = member->raw;
|
||||
}
|
||||
out->delay_source = delay_source;
|
||||
out->metadata_json = metadata_text;
|
||||
out->coefficients = std::move(coefficients);
|
||||
out->delay_coefficients = std::move(delay_coefficients);
|
||||
out->delay_bounds = std::move(delay_bounds);
|
||||
out->band_centers_hz = std::move(centers);
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status load_kernels(const std::string& npz_path, Kernels* out) {
|
||||
if (out == nullptr) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kRender, "null kernels");
|
||||
}
|
||||
io::ZipArchive archive;
|
||||
std::string error;
|
||||
if (!archive.open(npz_path, &error)) {
|
||||
return Status::fail(JOC_ERR_HRTF_NOT_FOUND, stage::kRender,
|
||||
"cannot read kernel tables " + npz_path + ": " + error);
|
||||
}
|
||||
|
||||
struct Request {
|
||||
const char* member;
|
||||
const char* shape_text;
|
||||
std::vector<std::int64_t> shape;
|
||||
};
|
||||
const Request requests[6] = {
|
||||
{"qmf_analysis_coefficients.npy", "<f4", {64, 10}},
|
||||
{"hybrid_analysis_low_kernel.npy", "<f4", {3, 2, 13, 16, 2}},
|
||||
{"hybrid_synthesis_indices.npy", "<i2", {154, 4}},
|
||||
{"hybrid_synthesis_values.npy", "<f4", {154}},
|
||||
{"qmf_synthesis_basis.npy", "<f8", {64, 4, 128}},
|
||||
{"qmf_synthesis_taps.npy", "<f8", {64, 10, 4}},
|
||||
};
|
||||
|
||||
std::vector<std::uint8_t> raw;
|
||||
std::vector<std::uint8_t> ordered;
|
||||
for (const Request& request : requests) {
|
||||
const std::string name = request.member;
|
||||
if (!archive.read_member(name, &raw, &error)) {
|
||||
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender,
|
||||
"kernel table member " + name + ": " + error);
|
||||
}
|
||||
io::NpyArray array;
|
||||
if (!io::parse_npy(raw.data(), raw.size(), &array, &error)) {
|
||||
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender,
|
||||
"kernel table member " + name + ": " + error);
|
||||
}
|
||||
if (array.descr != request.shape_text || !io::npy_shape_is(array, request.shape)) {
|
||||
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender,
|
||||
"kernel table member " + name + " has an unexpected dtype/shape");
|
||||
}
|
||||
// Logical C order: required because the reused kernel indexes the hybrid
|
||||
// synthesis table row-major while the shipped member is Fortran-order.
|
||||
if (!io::npy_to_c_order(array, &ordered, &error)) {
|
||||
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender,
|
||||
"kernel table member " + name + ": " + error);
|
||||
}
|
||||
const std::size_t count = array.element_count();
|
||||
if (std::strcmp(request.member, "qmf_analysis_coefficients.npy") == 0) {
|
||||
std::vector<float> values(count);
|
||||
std::memcpy(values.data(), ordered.data(), count * sizeof(float));
|
||||
out->qmf_analysis.assign(values.begin(), values.end());
|
||||
} else if (std::strcmp(request.member, "hybrid_analysis_low_kernel.npy") == 0) {
|
||||
std::vector<float> values(count);
|
||||
std::memcpy(values.data(), ordered.data(), count * sizeof(float));
|
||||
out->hybrid_low.assign(values.begin(), values.end());
|
||||
} else if (std::strcmp(request.member, "hybrid_synthesis_indices.npy") == 0) {
|
||||
out->hybrid_indices.resize(count);
|
||||
std::memcpy(out->hybrid_indices.data(), ordered.data(), count * sizeof(std::int16_t));
|
||||
} else if (std::strcmp(request.member, "hybrid_synthesis_values.npy") == 0) {
|
||||
std::vector<float> values(count);
|
||||
std::memcpy(values.data(), ordered.data(), count * sizeof(float));
|
||||
out->hybrid_values.assign(values.begin(), values.end());
|
||||
} else if (std::strcmp(request.member, "qmf_synthesis_basis.npy") == 0) {
|
||||
std::memcpy(out->qmf_basis.empty() ? (out->qmf_basis.resize(count), out->qmf_basis.data())
|
||||
: out->qmf_basis.data(),
|
||||
ordered.data(), count * sizeof(double));
|
||||
out->qmf_basis.resize(count);
|
||||
} else {
|
||||
out->qmf_taps.resize(count);
|
||||
std::memcpy(out->qmf_taps.data(), ordered.data(), count * sizeof(double));
|
||||
}
|
||||
}
|
||||
out->hybrid_count = static_cast<std::uint32_t>(out->hybrid_values.size());
|
||||
if (out->hybrid_count == 0u) {
|
||||
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender,
|
||||
"kernel tables contain no hybrid synthesis entries");
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
} // namespace joc::hrtf
|
||||
@@ -0,0 +1,66 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "foundation/status.h"
|
||||
|
||||
namespace joc::hrtf {
|
||||
|
||||
inline constexpr int kShTerms = 36;
|
||||
inline constexpr int kEars = 2;
|
||||
inline constexpr int kHybridBands = 77;
|
||||
inline constexpr int kFormatVersion = 1;
|
||||
inline constexpr const char* kMagic = "JOC-HRTF-CACHE";
|
||||
inline constexpr const char* kCacheSchema = "joc-compiled-hrtf-v1";
|
||||
|
||||
struct Field {
|
||||
std::vector<double> coefficients;
|
||||
std::vector<double> delay_coefficients;
|
||||
std::vector<double> delay_bounds;
|
||||
std::vector<double> band_centers_hz;
|
||||
double measurement_radius_m = 1.0;
|
||||
long long order = 5;
|
||||
std::string source_sha256;
|
||||
std::string cache_key;
|
||||
std::string payload_sha256;
|
||||
std::string delay_source;
|
||||
std::string compiler_version;
|
||||
std::string phase_policy_version;
|
||||
std::string sh_convention;
|
||||
std::string filterbank_json;
|
||||
std::string metadata_json;
|
||||
// Compile-side metadata, needed to write the cache back out unchanged.
|
||||
std::string source_display_name;
|
||||
std::string fit_report_json;
|
||||
double projection_ridge = 0.0;
|
||||
double spherical_harmonic_ridge = 0.0;
|
||||
};
|
||||
|
||||
Status load_jochrtf(const std::string& path, Field* out);
|
||||
|
||||
// Binaural filterbank kernels, as the reused kernel expects them (C order, the
|
||||
// exact dtypes of the ABI parameters).
|
||||
struct Kernels {
|
||||
std::vector<double> qmf_analysis;
|
||||
std::vector<double> hybrid_low;
|
||||
std::vector<std::int16_t> hybrid_indices;
|
||||
std::vector<double> hybrid_values;
|
||||
std::vector<double> qmf_basis;
|
||||
std::vector<double> qmf_taps;
|
||||
std::uint32_t hybrid_count = 0;
|
||||
};
|
||||
|
||||
// Loads a kernel-table archive. The file path is an override for verification;
|
||||
// the shipped tables are embedded (see builtin_kernels) so no data file is needed.
|
||||
// The Fortran-order index member is transposed into C order on purpose: the reused
|
||||
// kernel indexes the hybrid synthesis table row-major.
|
||||
Status load_kernels(const std::string& npz_path, Kernels* out);
|
||||
|
||||
// The public filterbank tables compiled into the library (identical values to the
|
||||
// archive the file loader accepts; the unit test checks their hashes).
|
||||
const Kernels& builtin_kernels();
|
||||
|
||||
} // namespace joc::hrtf
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,610 @@
|
||||
#include "hrtf/public_filterbank.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "foundation/fft.h"
|
||||
#include "simd/simd.h"
|
||||
|
||||
namespace joc::hrtf {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
constexpr std::size_t kQmfLength = dsp::kQmfFftSize;
|
||||
constexpr int kQmfTaps = 10;
|
||||
constexpr int kSynthesisRank = 4;
|
||||
constexpr int kSynthesisTaps = 10;
|
||||
|
||||
// ----------------------------------------------------------- filterbank -----
|
||||
|
||||
// One shared forward plan for the 128-point QMF transform. The analysis bank runs
|
||||
// it 2 * slots * channels times per chunk, so the twiddle recurrence is built once
|
||||
// instead of being re-derived inside every butterfly.
|
||||
const dsp::FftPlan& qmf_fft_plan() {
|
||||
static const dsp::FftPlan plan(dsp::kQmfFftSize, false);
|
||||
return plan;
|
||||
}
|
||||
|
||||
// Public 64-band complex QMF analysis (public_filterbank.QmfAnalysis).
|
||||
class QmfAnalysis {
|
||||
public:
|
||||
static_assert(static_cast<std::size_t>(kQmfBands) == simd::kQmfAnalysisBands,
|
||||
"the dispatched accumulate is written for this band count");
|
||||
QmfAnalysis(const Kernels& kernels, std::size_t channels)
|
||||
: channels_(channels), coefficients_(kernels.qmf_analysis) {
|
||||
history_.assign(9u * channels_ * kQmfBands, 0.0);
|
||||
// The polyphase MAC consumes one coefficient per band, so the shipped
|
||||
// [band][tap] layout makes its inner loop a stride-10 gather. Transposing
|
||||
// once here turns that into a contiguous AXPY. The coefficient values and
|
||||
// the accumulation order are untouched, so the sums are bit-identical.
|
||||
coefficients_by_lag_.resize(static_cast<std::size_t>(kQmfTaps) * kQmfBands);
|
||||
for (int band = 0; band < kQmfBands; ++band) {
|
||||
for (int tap = 0; tap < kQmfTaps; ++tap) {
|
||||
coefficients_by_lag_[static_cast<std::size_t>(tap) * kQmfBands +
|
||||
static_cast<std::size_t>(band)] =
|
||||
coefficients_[static_cast<std::size_t>(band) * kQmfTaps +
|
||||
static_cast<std::size_t>(tap)];
|
||||
}
|
||||
}
|
||||
premultiply_.resize(kQmfBands);
|
||||
post_.resize(kQmfBands);
|
||||
even_post_.resize(kQmfBands);
|
||||
for (int band = 0; band < kQmfBands; ++band) {
|
||||
const double phase = static_cast<double>(band);
|
||||
premultiply_[static_cast<std::size_t>(band)] =
|
||||
std::polar(1.0, -kPi * phase / 128.0);
|
||||
post_[static_cast<std::size_t>(band)] =
|
||||
std::polar(1.0, -3.0 * (phase + 0.5) * kPi / 128.0);
|
||||
even_post_[static_cast<std::size_t>(band)] =
|
||||
Complex(0.0, band % 2 == 0 ? 1.0 : -1.0);
|
||||
}
|
||||
}
|
||||
|
||||
void reset() { std::fill(history_.begin(), history_.end(), 0.0); }
|
||||
|
||||
// samples: [slots*64, channels]; output: [slots, channels, 64] complex.
|
||||
void process(const std::vector<double>& samples, std::size_t slots,
|
||||
std::vector<Complex>* output) {
|
||||
const std::size_t joined_slots = 9u + slots;
|
||||
const std::size_t history_size = 9u * channels_ * kQmfBands;
|
||||
const std::size_t joined_size = joined_slots * channels_ * kQmfBands;
|
||||
// The joined window is filled completely -- the history lands in its first
|
||||
// 9 * channels * 64 entries and the new samples in the rest -- so it is a
|
||||
// reusable scratch buffer rather than a fresh zero-filled allocation. The
|
||||
// history tail is taken by index instead of from end(), because the buffer may
|
||||
// be longer than the window this call uses.
|
||||
if (joined_.size() < joined_size) {
|
||||
joined_.resize(joined_size);
|
||||
}
|
||||
std::copy(history_.begin(), history_.end(), joined_.begin());
|
||||
std::copy(samples.begin(), samples.begin() + static_cast<std::ptrdiff_t>(slots * channels_ * kQmfBands),
|
||||
joined_.begin() + static_cast<std::ptrdiff_t>(history_size));
|
||||
|
||||
// The two polyphase accumulators are read before they are written, so their
|
||||
// zero fill is load-bearing and stays; only the per-call allocation goes.
|
||||
const std::size_t accumulator_size = slots * channels_ * kQmfBands;
|
||||
if (even_.size() < accumulator_size) {
|
||||
even_.resize(accumulator_size);
|
||||
}
|
||||
if (odd_.size() < accumulator_size) {
|
||||
odd_.resize(accumulator_size);
|
||||
}
|
||||
std::fill(even_.begin(), even_.begin() + static_cast<std::ptrdiff_t>(accumulator_size), 0.0);
|
||||
std::fill(odd_.begin(), odd_.begin() + static_cast<std::ptrdiff_t>(accumulator_size), 0.0);
|
||||
// The ten lags are ten accumulate passes over the same 64 bands with one
|
||||
// shared coefficient row; the bands are independent accumulations of a
|
||||
// single product each, so they are what the dispatched kernel puts in its
|
||||
// lanes, and every band keeps the caller's own multiply-then-add.
|
||||
//
|
||||
// Slots are processed in blocks, with the lag loop inside: one lag pass
|
||||
// touches every source row once, so running the ten passes over the whole
|
||||
// chunk re-reads the joined window ten times -- at 1536 slots that is
|
||||
// hundreds of megabytes per chunk and the loop ends up bound by memory, not
|
||||
// by arithmetic. A block's ten lag passes instead slide over a window of
|
||||
// (block + 9) rows that stays in the second-level cache. Lags still run in
|
||||
// ascending order inside a block, which is the order each output's sum is
|
||||
// formed in, so nothing about the arithmetic changes.
|
||||
constexpr std::size_t kSlotBlock = 32;
|
||||
for (std::size_t first = 0u; first < slots; first += kSlotBlock) {
|
||||
const std::size_t block = std::min(kSlotBlock, slots - first);
|
||||
for (int lag = 0; lag < kQmfTaps; ++lag) {
|
||||
std::vector<double>& target = (lag % 2 == 0) ? even_ : odd_;
|
||||
const double* row =
|
||||
coefficients_by_lag_.data() + static_cast<std::size_t>(lag) * kQmfBands;
|
||||
const std::size_t source_slot = 9u - static_cast<std::size_t>(lag) + first;
|
||||
simd::qmf_analysis_taps(
|
||||
target.data() + first * channels_ * kQmfBands,
|
||||
joined_.data() + source_slot * channels_ * kQmfBands, row,
|
||||
block * channels_);
|
||||
}
|
||||
}
|
||||
std::copy(joined_.begin() + static_cast<std::ptrdiff_t>(joined_size - history_size),
|
||||
joined_.begin() + static_cast<std::ptrdiff_t>(joined_size), history_.begin());
|
||||
|
||||
// Every output element is assigned below, so the size is all that has to be
|
||||
// established; a resize of an already correctly sized buffer touches nothing.
|
||||
output->resize(slots * channels_ * kQmfBands);
|
||||
std::array<Complex, dsp::kQmfFftSize> even_spectrum{};
|
||||
std::array<Complex, dsp::kQmfFftSize> odd_spectrum{};
|
||||
for (std::size_t slot = 0u; slot < slots; ++slot) {
|
||||
for (std::size_t channel = 0u; channel < channels_; ++channel) {
|
||||
const double* even_values = even_.data() + (slot * channels_ + channel) * kQmfBands;
|
||||
const double* odd_values = odd_.data() + (slot * channels_ + channel) * kQmfBands;
|
||||
transform(even_values, &even_spectrum);
|
||||
transform(odd_values, &odd_spectrum);
|
||||
Complex* destination =
|
||||
output->data() + (slot * channels_ + channel) * kQmfBands;
|
||||
for (int band = 0; band < kQmfBands; ++band) {
|
||||
destination[band] = odd_spectrum[static_cast<std::size_t>(band)] +
|
||||
even_spectrum[static_cast<std::size_t>(band)] *
|
||||
even_post_[static_cast<std::size_t>(band)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void transform(const double* values, std::array<Complex, dsp::kQmfFftSize>* spectrum) {
|
||||
for (int band = 0; band < kQmfBands; ++band) {
|
||||
(*spectrum)[static_cast<std::size_t>(band)] =
|
||||
Complex(values[band], 0.0) * premultiply_[static_cast<std::size_t>(band)];
|
||||
}
|
||||
for (int index = kQmfBands; index < dsp::kQmfFftSize; ++index) {
|
||||
(*spectrum)[static_cast<std::size_t>(index)] = Complex(0.0, 0.0);
|
||||
}
|
||||
dsp::fft_radix2(spectrum, qmf_fft_plan());
|
||||
for (int band = 0; band < kQmfBands; ++band) {
|
||||
(*spectrum)[static_cast<std::size_t>(band)] *= post_[static_cast<std::size_t>(band)];
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t channels_;
|
||||
std::vector<double> coefficients_; // [64][10]
|
||||
std::vector<double> coefficients_by_lag_; // [10][64], the same values transposed
|
||||
std::vector<double> history_; // [9][channels][64]
|
||||
std::vector<double> joined_; // scratch, [9 + slots][channels][64]
|
||||
std::vector<double> even_; // scratch, [slots][channels][64], zeroed per call
|
||||
std::vector<double> odd_; // scratch, [slots][channels][64], zeroed per call
|
||||
std::vector<Complex> premultiply_;
|
||||
std::vector<Complex> post_;
|
||||
std::vector<Complex> even_post_;
|
||||
};
|
||||
|
||||
// Sparse 64-QMF to 77-hybrid analysis (public_filterbank.HybridAnalysis).
|
||||
class HybridAnalysis {
|
||||
public:
|
||||
HybridAnalysis(const Kernels& kernels, std::size_t channels)
|
||||
: channels_(channels), low_kernel_(kernels.hybrid_low) {
|
||||
history_.assign(12u * channels_ * 3u * 2u, 0.0);
|
||||
high_history_.assign(6u * channels_ * 61u, Complex(0.0, 0.0));
|
||||
// The dispatched join walks one term at a time and adds its 32 weights to
|
||||
// 32 outputs, so the shipped [tap][band][component] table is regrouped to
|
||||
// the term order the caller accumulates in. Same weights, same order.
|
||||
const std::size_t outputs = simd::kHybridOutputs;
|
||||
low_by_term_.resize(simd::kHybridTerms * outputs);
|
||||
for (int lag = 0; lag < 13; ++lag) {
|
||||
for (int point = 0; point < 3; ++point) {
|
||||
for (int input = 0; input < 2; ++input) {
|
||||
const std::size_t term =
|
||||
(static_cast<std::size_t>(lag) * 3u + static_cast<std::size_t>(point)) * 2u +
|
||||
static_cast<std::size_t>(input);
|
||||
const std::size_t source = (static_cast<std::size_t>(point) * 2u +
|
||||
static_cast<std::size_t>(input)) * 13u +
|
||||
static_cast<std::size_t>(lag);
|
||||
for (std::size_t output = 0u; output < outputs; ++output) {
|
||||
low_by_term_[term * outputs + output] =
|
||||
low_kernel_[source * outputs + output];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
low_values_.resize(simd::kHybridJoinBlock * simd::kHybridTerms);
|
||||
low_out_.resize(simd::kHybridJoinBlock * outputs);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
std::fill(history_.begin(), history_.end(), 0.0);
|
||||
std::fill(high_history_.begin(), high_history_.end(), Complex(0.0, 0.0));
|
||||
}
|
||||
|
||||
// qmf: [slots, channels, 64]; output: [slots, channels, 77] complex.
|
||||
void process(const std::vector<Complex>& qmf, std::size_t slots,
|
||||
std::vector<Complex>* output) {
|
||||
const std::size_t joined_slots = 12u + slots;
|
||||
const std::size_t history_size = 12u * channels_ * 6u;
|
||||
const std::size_t joined_size = joined_slots * channels_ * 3u * 2u;
|
||||
// Both the joined window and the pending high-band history are written in full
|
||||
// before they are read, so they are reused scratch buffers; the history tail is
|
||||
// taken by index because the buffer can be longer than this call's window.
|
||||
if (joined_.size() < joined_size) {
|
||||
joined_.resize(joined_size);
|
||||
}
|
||||
std::copy(history_.begin(), history_.end(), joined_.begin());
|
||||
for (std::size_t slot = 0u; slot < slots; ++slot) {
|
||||
for (std::size_t channel = 0u; channel < channels_; ++channel) {
|
||||
const Complex* source = qmf.data() + (slot * channels_ + channel) * kQmfBands;
|
||||
double* destination =
|
||||
joined_.data() + ((12u + slot) * channels_ + channel) * 6u;
|
||||
for (int band = 0; band < 3; ++band) {
|
||||
destination[static_cast<std::size_t>(band) * 2u] = source[band].real();
|
||||
destination[static_cast<std::size_t>(band) * 2u + 1u] = source[band].imag();
|
||||
}
|
||||
}
|
||||
}
|
||||
// The low bands are accumulated in a register block and written straight into
|
||||
// the output, and the high bands are written by the pass below; between them
|
||||
// every one of the 77 bands is assigned, so only the size has to be set.
|
||||
output->resize(slots * channels_ * kHybridBands);
|
||||
// The thirteen taps are summed in a per-output register block and the low
|
||||
// bands are written straight into the output. Keeping a separate low plane
|
||||
// and then copying it into the output re-streams tens of megabytes per chunk
|
||||
// for nothing, and only the first kHybridLow bands are ever touched. The
|
||||
// join itself is dispatched (see src/simd/simd.h): the 32 outputs of a
|
||||
// row are 32 independent accumulations over the same 78 terms, which is what
|
||||
// shares a vector. Every lane keeps the caller's term order -- lag, then
|
||||
// point, then input -- and its two roundings, and skips exactly the terms
|
||||
// this loop skips. Rows are staged in blocks so the gathered values do not
|
||||
// spill out of the first-level cache.
|
||||
const std::size_t hybrid_rows = slots * channels_;
|
||||
const std::size_t block = simd::kHybridJoinBlock;
|
||||
const std::size_t terms = simd::kHybridTerms;
|
||||
for (std::size_t first = 0u; first < hybrid_rows; first += block) {
|
||||
const std::size_t count = std::min(block, hybrid_rows - first);
|
||||
for (std::size_t index = 0u; index < count; ++index) {
|
||||
const std::size_t row = first + index;
|
||||
const std::size_t slot = row / channels_;
|
||||
const std::size_t channel = row % channels_;
|
||||
double* staged = low_values_.data() + index * terms;
|
||||
for (int lag = 0; lag < 13; ++lag) {
|
||||
const std::size_t source_slot = 12u - static_cast<std::size_t>(lag) + slot;
|
||||
const double* source =
|
||||
joined_.data() + (source_slot * channels_ + channel) * 6u;
|
||||
for (int point = 0; point < 3; ++point) {
|
||||
for (int input = 0; input < 2; ++input) {
|
||||
staged[(static_cast<std::size_t>(lag) * 3u +
|
||||
static_cast<std::size_t>(point)) * 2u +
|
||||
static_cast<std::size_t>(input)] =
|
||||
source[static_cast<std::size_t>(point) * 2u +
|
||||
static_cast<std::size_t>(input)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
simd::hybrid_low_join(low_values_.data(), low_by_term_.data(),
|
||||
low_out_.data(), count);
|
||||
for (std::size_t index = 0u; index < count; ++index) {
|
||||
Complex* destination = output->data() + (first + index) * kHybridBands;
|
||||
const double* values = low_out_.data() + index * simd::kHybridOutputs;
|
||||
for (int band = 0; band < kHybridLow; ++band) {
|
||||
destination[band] = Complex(values[static_cast<std::size_t>(band) * 2u],
|
||||
values[static_cast<std::size_t>(band) * 2u + 1u]);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::copy(joined_.begin() + static_cast<std::ptrdiff_t>(joined_size - history_size),
|
||||
joined_.begin() + static_cast<std::ptrdiff_t>(joined_size), history_.begin());
|
||||
|
||||
// The high bands pass through unchanged but delayed by the six slots of
|
||||
// history the reference concatenates in front of them. Only the last six
|
||||
// entries of that concatenation survive into high_history_, so a six-entry
|
||||
// register replaces the (6 + slots) plane and its full copy. Note the
|
||||
// output reads the concatenation at index `slot`, not `6 + slot`, so the
|
||||
// first six output slots come from the history: that offset is part of the
|
||||
// current output and is preserved verbatim.
|
||||
for (std::size_t slot = 0u; slot < slots; ++slot) {
|
||||
for (std::size_t channel = 0u; channel < channels_; ++channel) {
|
||||
Complex* destination = output->data() +
|
||||
(slot * channels_ + channel) * kHybridBands + kHybridLow;
|
||||
if (slot < 6u) {
|
||||
const Complex* source =
|
||||
high_history_.data() + (slot * channels_ + channel) * 61u;
|
||||
for (int band = 0; band < 61; ++band) {
|
||||
destination[band] = source[band];
|
||||
}
|
||||
} else {
|
||||
const Complex* source =
|
||||
qmf.data() + ((slot - 6u) * channels_ + channel) * kQmfBands;
|
||||
for (int band = 3; band < kQmfBands; ++band) {
|
||||
destination[static_cast<std::size_t>(band - 3)] = source[band];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Every entry of the pending high-band history is written here, so it is a
|
||||
// reusable scratch buffer; the copy into the live history is kept as it was.
|
||||
if (next_high_history_.size() < 6u * channels_ * 61u) {
|
||||
next_high_history_.resize(6u * channels_ * 61u);
|
||||
}
|
||||
for (std::size_t entry = 0u; entry < 6u; ++entry) {
|
||||
const std::size_t combined = slots + entry;
|
||||
for (std::size_t channel = 0u; channel < channels_; ++channel) {
|
||||
Complex* destination =
|
||||
next_high_history_.data() + (entry * channels_ + channel) * 61u;
|
||||
if (combined < 6u) {
|
||||
const Complex* source =
|
||||
high_history_.data() + (combined * channels_ + channel) * 61u;
|
||||
for (int band = 0; band < 61; ++band) {
|
||||
destination[band] = source[band];
|
||||
}
|
||||
} else {
|
||||
const Complex* source =
|
||||
qmf.data() + ((combined - 6u) * channels_ + channel) * kQmfBands;
|
||||
for (int band = 3; band < kQmfBands; ++band) {
|
||||
destination[static_cast<std::size_t>(band - 3)] = source[band];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::copy(next_high_history_.begin(), next_high_history_.end(), high_history_.begin());
|
||||
}
|
||||
|
||||
private:
|
||||
std::size_t channels_;
|
||||
std::vector<double> low_kernel_; // [3][2][13][16][2]
|
||||
std::vector<double> low_by_term_; // [78][32], the same weights in the caller's term order
|
||||
std::vector<double> low_values_; // scratch, [block][78]
|
||||
std::vector<double> low_out_; // scratch, [block][32]
|
||||
std::vector<double> history_; // [12][channels][3][2]
|
||||
std::vector<Complex> high_history_; // [6][channels][61]
|
||||
std::vector<double> joined_; // scratch, [12 + slots][channels][3][2]
|
||||
std::vector<Complex> next_high_history_; // scratch, [6][channels][61]
|
||||
};
|
||||
|
||||
// Instantaneous sparse 77-hybrid to 64-QMF synthesis map.
|
||||
class HybridSynthesis {
|
||||
public:
|
||||
explicit HybridSynthesis(const Kernels& kernels) {
|
||||
const std::size_t rows = kernels.hybrid_indices.size() / 4u;
|
||||
mapping_.reserve(rows);
|
||||
for (std::size_t index = 0u; index < rows; ++index) {
|
||||
Entry entry;
|
||||
for (int field = 0; field < 4; ++field) {
|
||||
entry.index[static_cast<std::size_t>(field)] =
|
||||
kernels.hybrid_indices[index * 4u + static_cast<std::size_t>(field)];
|
||||
}
|
||||
entry.gain = kernels.hybrid_values[index];
|
||||
mapping_.push_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// hybrid: [slots, channels, 77]; output: [slots, channels, 64] complex.
|
||||
// The sparse map moves a real or imaginary part of one band into a real or
|
||||
// imaginary part of another, so the two components are accumulated apart.
|
||||
void process(const std::vector<Complex>& hybrid, std::size_t slots, std::size_t channels,
|
||||
std::vector<Complex>* output) const {
|
||||
const std::size_t rows = slots * channels;
|
||||
std::vector<double> real(rows * kQmfBands, 0.0);
|
||||
std::vector<double> imaginary(rows * kQmfBands, 0.0);
|
||||
for (std::size_t slot = 0u; slot < slots; ++slot) {
|
||||
for (std::size_t channel = 0u; channel < channels; ++channel) {
|
||||
const std::size_t row = slot * channels + channel;
|
||||
const Complex* source = hybrid.data() + row * kHybridBands;
|
||||
for (const Entry& entry : mapping_) {
|
||||
const double value = entry.index[1] == 0u ? source[entry.index[0]].real()
|
||||
: source[entry.index[0]].imag();
|
||||
if (value == 0.0) {
|
||||
continue;
|
||||
}
|
||||
double* destination =
|
||||
(entry.index[3] == 0u ? real.data() : imaginary.data()) + row * kQmfBands;
|
||||
destination[entry.index[2]] += value * entry.gain;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Every output element is assigned from the two accumulators below, so the
|
||||
// zero fill that `assign` performed was dead; only the size is needed.
|
||||
output->resize(rows * kQmfBands);
|
||||
for (std::size_t index = 0u; index < output->size(); ++index) {
|
||||
(*output)[index] = Complex(real[index], imaginary[index]);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
std::size_t index[4] = {0u, 0u, 0u, 0u};
|
||||
double gain = 0.0;
|
||||
};
|
||||
std::vector<Entry> mapping_;
|
||||
};
|
||||
|
||||
// Rank-4 64-band synthesis.
|
||||
class QmfSynthesis {
|
||||
public:
|
||||
QmfSynthesis(const Kernels& kernels, std::size_t channels)
|
||||
: channels_(channels), basis_(kernels.qmf_basis), taps_(kernels.qmf_taps) {
|
||||
history_.assign(9u * channels_ * kQmfBands * kSynthesisRank, 0.0);
|
||||
// The dispatched basis kernel reads the four ranks of one (band, tap) as
|
||||
// one vector, so the shipped [band][rank][tap] table is reordered once
|
||||
// here. The weights are the same doubles, only their order differs.
|
||||
const std::size_t bands = static_cast<std::size_t>(kQmfBands);
|
||||
const std::size_t ranks = static_cast<std::size_t>(kSynthesisRank);
|
||||
const std::size_t taps = dsp::kQmfFftSize;
|
||||
basis_by_tap_.resize(bands * taps * ranks);
|
||||
for (std::size_t band = 0u; band < bands; ++band) {
|
||||
for (std::size_t tap = 0u; tap < taps; ++tap) {
|
||||
for (std::size_t rank = 0u; rank < ranks; ++rank) {
|
||||
basis_by_tap_[(band * taps + tap) * ranks + rank] =
|
||||
basis_[(band * ranks + rank) * taps + tap];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void reset() { std::fill(history_.begin(), history_.end(), 0.0); }
|
||||
|
||||
// qmf: [slots, channels, 64]; output: [slots*64, channels] real.
|
||||
void process(const std::vector<Complex>& qmf, std::size_t slots, std::vector<double>* output) {
|
||||
const std::size_t rows = slots * channels_;
|
||||
// [row][band][component] staging for the basis application. Both staging
|
||||
// planes and the joined window are reusable scratch: every element of each is
|
||||
// written before it is read, so the buffers are sized once and kept instead of
|
||||
// being allocated and zero-filled on every call.
|
||||
const std::size_t flat_size = rows * dsp::kQmfFftSize;
|
||||
if (flat_.size() < flat_size) {
|
||||
flat_.resize(flat_size);
|
||||
}
|
||||
for (std::size_t row = 0u; row < rows; ++row) {
|
||||
for (int band = 0; band < kQmfBands; ++band) {
|
||||
flat_[row * dsp::kQmfFftSize + static_cast<std::size_t>(band) * 2u] =
|
||||
qmf[row * kQmfBands + static_cast<std::size_t>(band)].real();
|
||||
flat_[row * dsp::kQmfFftSize + static_cast<std::size_t>(band) * 2u + 1u] =
|
||||
qmf[row * kQmfBands + static_cast<std::size_t>(band)].imag();
|
||||
}
|
||||
}
|
||||
// The sums are written straight into the joined window: the destination index
|
||||
// is known up front, the summation order is untouched, and the application
|
||||
// itself is dispatched -- the four ranks of a band are four independent dot
|
||||
// products over the same 128 values, so they share a vector while every lane
|
||||
// keeps the tap order and the two roundings of `sum +=`.
|
||||
const std::size_t history_size = 9u * channels_ * kQmfBands * kSynthesisRank;
|
||||
const std::size_t joined_size = history_size + rows * kQmfBands * kSynthesisRank;
|
||||
if (joined_.size() < joined_size) {
|
||||
joined_.resize(joined_size);
|
||||
}
|
||||
std::copy(history_.begin(), history_.end(), joined_.begin());
|
||||
simd::qmf_synthesis_basis(flat_.data(), basis_by_tap_.data(),
|
||||
joined_.data() + history_size, rows);
|
||||
std::copy(joined_.begin() + static_cast<std::ptrdiff_t>(joined_size - history_size),
|
||||
joined_.begin() + static_cast<std::ptrdiff_t>(joined_size), history_.begin());
|
||||
|
||||
output->assign(rows * kQmfBands, 0.0);
|
||||
for (int lag = 0; lag < kSynthesisTaps; ++lag) {
|
||||
for (std::size_t slot = 0u; slot < slots; ++slot) {
|
||||
const std::size_t source_slot = 9u - static_cast<std::size_t>(lag) + slot;
|
||||
for (std::size_t channel = 0u; channel < channels_; ++channel) {
|
||||
const double* source =
|
||||
joined_.data() +
|
||||
(source_slot * channels_ + channel) * kQmfBands * kSynthesisRank;
|
||||
double* destination =
|
||||
output->data() + (slot * channels_ + channel) * kQmfBands;
|
||||
for (int band = 0; band < kQmfBands; ++band) {
|
||||
double sum = 0.0;
|
||||
for (int rank = 0; rank < kSynthesisRank; ++rank) {
|
||||
sum += source[static_cast<std::size_t>(band) * kSynthesisRank +
|
||||
static_cast<std::size_t>(rank)] *
|
||||
taps_[(static_cast<std::size_t>(band) * kSynthesisTaps +
|
||||
static_cast<std::size_t>(lag)) * kSynthesisRank +
|
||||
static_cast<std::size_t>(rank)];
|
||||
}
|
||||
destination[band] += sum;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::size_t channels_;
|
||||
std::vector<double> basis_; // [64][4][128]
|
||||
std::vector<double> basis_by_tap_; // [64][128][4], the same weights transposed
|
||||
std::vector<double> taps_; // [64][10][4]
|
||||
std::vector<double> history_; // [9][channels][64][4]
|
||||
std::vector<double> flat_; // scratch, [rows][128], fully written per call
|
||||
std::vector<double> joined_; // scratch, [9 + slots][channels][64][4]
|
||||
};
|
||||
|
||||
// public_filterbank.PublicAnalysis77.process: [N, channels] -> [N/64, channels, 77].
|
||||
void analysis_77(const std::vector<double>& samples, std::size_t slots,
|
||||
std::size_t channels, QmfAnalysis& qmf,
|
||||
HybridAnalysis& hybrid_analysis, std::vector<Complex>* hybrid) {
|
||||
std::vector<double> hops(slots * channels * kQmfHop, 0.0);
|
||||
for (std::size_t slot = 0u; slot < slots; ++slot) {
|
||||
for (std::size_t channel = 0u; channel < channels; ++channel) {
|
||||
for (int index = 0; index < kQmfHop; ++index) {
|
||||
hops[(slot * channels + channel) * kQmfHop + static_cast<std::size_t>(index)] =
|
||||
samples[(slot * kQmfHop + static_cast<std::size_t>(index)) * channels + channel];
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<Complex> qmf_bands;
|
||||
qmf.process(hops, slots, &qmf_bands);
|
||||
hybrid_analysis.process(qmf_bands, slots, hybrid);
|
||||
}
|
||||
|
||||
// public_filterbank.PublicSynthesis77.process: [slots, channels, 77] -> [slots*64, channels].
|
||||
void synthesis_77(const std::vector<Complex>& hybrid, std::size_t slots,
|
||||
std::size_t channels, const HybridSynthesis& synthesis,
|
||||
QmfSynthesis& qmf, std::vector<double>* time) {
|
||||
std::vector<Complex> qmf_bands;
|
||||
synthesis.process(hybrid, slots, channels, &qmf_bands);
|
||||
std::vector<double> samples;
|
||||
qmf.process(qmf_bands, slots, &samples);
|
||||
// The reference transposes (slots, channels, 64) to sample-major output.
|
||||
time->assign(samples.size(), 0.0);
|
||||
for (std::size_t slot = 0u; slot < slots; ++slot) {
|
||||
for (std::size_t channel = 0u; channel < channels; ++channel) {
|
||||
for (int band = 0; band < kQmfBands; ++band) {
|
||||
(*time)[(slot * kQmfHop + static_cast<std::size_t>(band)) * channels + channel] =
|
||||
samples[(slot * channels + channel) * kQmfBands + static_cast<std::size_t>(band)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct PublicFilterbank::Impl {
|
||||
Impl(const Kernels& kernels, std::size_t channels)
|
||||
: channels(channels), qmf(kernels, channels), hybrid_analysis(kernels, channels),
|
||||
hybrid_synthesis(kernels), qmf_synthesis(kernels, channels) {}
|
||||
|
||||
std::size_t channels;
|
||||
QmfAnalysis qmf;
|
||||
HybridAnalysis hybrid_analysis;
|
||||
HybridSynthesis hybrid_synthesis;
|
||||
QmfSynthesis qmf_synthesis;
|
||||
};
|
||||
|
||||
PublicFilterbank::PublicFilterbank(const Kernels& kernels, std::size_t channels)
|
||||
: impl_(std::make_unique<Impl>(kernels, channels)) {}
|
||||
|
||||
PublicFilterbank::~PublicFilterbank() = default;
|
||||
|
||||
void PublicFilterbank::reset() {
|
||||
impl_->qmf.reset();
|
||||
impl_->hybrid_analysis.reset();
|
||||
impl_->qmf_synthesis.reset();
|
||||
}
|
||||
|
||||
void PublicFilterbank::analyze_full_rate(const std::vector<double>& samples, std::size_t slots,
|
||||
std::vector<Complex>* hybrid) {
|
||||
analysis_77(samples, slots, impl_->channels, impl_->qmf, impl_->hybrid_analysis, hybrid);
|
||||
}
|
||||
|
||||
void PublicFilterbank::synthesize_full_rate(const std::vector<Complex>& hybrid, std::size_t slots,
|
||||
std::vector<double>* time) {
|
||||
synthesis_77(hybrid, slots, impl_->channels, impl_->hybrid_synthesis, impl_->qmf_synthesis,
|
||||
time);
|
||||
}
|
||||
|
||||
void PublicFilterbank::analyze_qmf(const std::vector<double>& hops, std::size_t slots,
|
||||
std::vector<Complex>* qmf) {
|
||||
impl_->qmf.process(hops, slots, qmf);
|
||||
}
|
||||
|
||||
void PublicFilterbank::analyze_hybrid(const std::vector<Complex>& qmf, std::size_t slots,
|
||||
std::vector<Complex>* hybrid) {
|
||||
impl_->hybrid_analysis.process(qmf, slots, hybrid);
|
||||
}
|
||||
|
||||
void PublicFilterbank::synthesize_hybrid(const std::vector<Complex>& hybrid, std::size_t slots,
|
||||
std::vector<Complex>* qmf) {
|
||||
impl_->hybrid_synthesis.process(hybrid, slots, impl_->channels, qmf);
|
||||
}
|
||||
|
||||
void PublicFilterbank::synthesize_qmf(const std::vector<Complex>& qmf, std::size_t slots,
|
||||
std::vector<double>* time) {
|
||||
impl_->qmf_synthesis.process(qmf, slots, time);
|
||||
}
|
||||
|
||||
} // namespace joc::hrtf
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <complex>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "hrtf/jochrtf.h"
|
||||
|
||||
// Public 64-QMF / 77-hybrid filterbank, shared by the SOFA field compiler and the
|
||||
// Rosella renderer (upstream public_filterbank.py and rosella_filterbank.py are
|
||||
// the same bank). Everything is float64/complex128, as the reference computes it,
|
||||
// and the stateful half-steps are exposed because Rosella drives them directly.
|
||||
namespace joc::hrtf {
|
||||
|
||||
inline constexpr int kQmfBands = 64;
|
||||
inline constexpr int kQmfHop = 64;
|
||||
inline constexpr int kHybridLow = 16;
|
||||
inline constexpr int kHybridBandCount = 77;
|
||||
inline constexpr int kLatencySamples = 961;
|
||||
|
||||
using Complex = std::complex<double>;
|
||||
|
||||
class PublicFilterbank {
|
||||
public:
|
||||
PublicFilterbank(const Kernels& kernels, std::size_t channels);
|
||||
~PublicFilterbank();
|
||||
PublicFilterbank(const PublicFilterbank&) = delete;
|
||||
PublicFilterbank& operator=(const PublicFilterbank&) = delete;
|
||||
|
||||
void reset();
|
||||
|
||||
// Full-rate [slots*64, channels] -> hybrid [slots, channels, 77].
|
||||
void analyze_full_rate(const std::vector<double>& samples, std::size_t slots,
|
||||
std::vector<Complex>* hybrid);
|
||||
// Hybrid [slots, channels, 77] -> full-rate [slots*64, channels].
|
||||
void synthesize_full_rate(const std::vector<Complex>& hybrid, std::size_t slots,
|
||||
std::vector<double>* time);
|
||||
|
||||
// The stateful half-steps, in the order the reference runs them.
|
||||
void analyze_qmf(const std::vector<double>& hops, std::size_t slots,
|
||||
std::vector<Complex>* qmf);
|
||||
void analyze_hybrid(const std::vector<Complex>& qmf, std::size_t slots,
|
||||
std::vector<Complex>* hybrid);
|
||||
void synthesize_hybrid(const std::vector<Complex>& hybrid, std::size_t slots,
|
||||
std::vector<Complex>* qmf);
|
||||
void synthesize_qmf(const std::vector<Complex>& qmf, std::size_t slots,
|
||||
std::vector<double>* time);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace joc::hrtf
|
||||
@@ -0,0 +1,535 @@
|
||||
#include "hrtf/rosella_model.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "foundation/fs_utf8.h"
|
||||
#include "foundation/mini_json.h"
|
||||
#include "foundation/sha256.h"
|
||||
|
||||
namespace joc::hrtf {
|
||||
|
||||
namespace {
|
||||
|
||||
// The model's fixed-point lane scale: every stored value is a Q15 integer.
|
||||
constexpr float kQ15 = 1.0f / 32768.0f;
|
||||
|
||||
Status model_fail(joc_error code, const std::string& message) {
|
||||
return Status::fail(code, stage::kRender, message);
|
||||
}
|
||||
|
||||
float q15(std::int32_t value) { return static_cast<float>(value) * kQ15; }
|
||||
|
||||
float q15_exp(std::int32_t value, int exponent) {
|
||||
return q15(value) * static_cast<float>(std::ldexp(1.0, exponent));
|
||||
}
|
||||
|
||||
std::uint16_t low16(std::int32_t value) {
|
||||
return static_cast<std::uint16_t>(static_cast<std::uint32_t>(value) & 0xFFFFu);
|
||||
}
|
||||
|
||||
std::string trim(const std::string& text) {
|
||||
const std::size_t begin = text.find_first_not_of(" \t\r\n");
|
||||
const std::size_t end = text.find_last_not_of(" \t\r\n");
|
||||
return begin == std::string::npos ? std::string() : text.substr(begin, end - begin + 1u);
|
||||
}
|
||||
|
||||
// The lane array is read straight out of the JSON text: it is one flat list of
|
||||
// integers, and building a 15691-node DOM for it would only cost time.
|
||||
bool parse_int_array(const std::string& raw, std::vector<std::int32_t>* out, std::string* error) {
|
||||
out->clear();
|
||||
const char* cursor = raw.c_str();
|
||||
const char* end = cursor + raw.size();
|
||||
while (cursor < end && *cursor != '[') {
|
||||
++cursor;
|
||||
}
|
||||
if (cursor == end) {
|
||||
*error = "rosella_coefficients must be a JSON array";
|
||||
return false;
|
||||
}
|
||||
++cursor;
|
||||
while (cursor < end) {
|
||||
while (cursor < end && (*cursor == ' ' || *cursor == '\t' || *cursor == '\r' ||
|
||||
*cursor == '\n' || *cursor == ',')) {
|
||||
++cursor;
|
||||
}
|
||||
if (cursor >= end) {
|
||||
break;
|
||||
}
|
||||
if (*cursor == ']') {
|
||||
return true;
|
||||
}
|
||||
const bool negative = *cursor == '-';
|
||||
if (negative) {
|
||||
++cursor;
|
||||
}
|
||||
if (cursor >= end || *cursor < '0' || *cursor > '9') {
|
||||
*error = "rosella_coefficients contains a non-integer value";
|
||||
return false;
|
||||
}
|
||||
long long value = 0;
|
||||
while (cursor < end && *cursor >= '0' && *cursor <= '9') {
|
||||
value = value * 10 + (*cursor - '0');
|
||||
if (value > (1ll << 40)) {
|
||||
*error = "rosella_coefficients value is out of range";
|
||||
return false;
|
||||
}
|
||||
++cursor;
|
||||
}
|
||||
// A fractional part or an exponent means the value is not an exact integer.
|
||||
if (cursor < end && (*cursor == '.' || *cursor == 'e' || *cursor == 'E')) {
|
||||
*error = "rosella_coefficients contains a non-integer value";
|
||||
return false;
|
||||
}
|
||||
if (negative) {
|
||||
value = -value;
|
||||
}
|
||||
if (value < -(1ll << 31) || value > (1ll << 31) - 1) {
|
||||
*error = "rosella_coefficients value is outside signed int32";
|
||||
return false;
|
||||
}
|
||||
out->push_back(static_cast<std::int32_t>(value));
|
||||
}
|
||||
*error = "rosella_coefficients array is truncated";
|
||||
return false;
|
||||
}
|
||||
|
||||
struct RpHeader {
|
||||
std::uint16_t stored_checksum = 0;
|
||||
std::uint16_t computed_checksum = 0;
|
||||
bool checksum_valid = false;
|
||||
bool table_a_present = false;
|
||||
bool table_b_present = false;
|
||||
bool table_c_present = false;
|
||||
int table_a_dimension = 0;
|
||||
int table_a_option = 0;
|
||||
int table_a_extra = 0;
|
||||
int table_b_dimension = 0;
|
||||
int table_b_extra = 0;
|
||||
int table_b_groups = 0;
|
||||
int table_c_dimension = 0;
|
||||
std::size_t active_lanes = 0;
|
||||
};
|
||||
|
||||
Status inspect_rp(const std::vector<std::int32_t>& lanes, RpHeader* out) {
|
||||
if (lanes.size() < 5u) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT, "Rosella rp must contain whole int32 lanes");
|
||||
}
|
||||
if (low16(lanes[0]) != 0x7072u) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT, "bad Rosella rp magic");
|
||||
}
|
||||
out->stored_checksum = low16(lanes[1]);
|
||||
out->table_a_present = low16(lanes[2]) != 0u;
|
||||
out->table_b_present = low16(lanes[3]) != 0u;
|
||||
out->table_c_present = low16(lanes[4]) != 0u;
|
||||
std::size_t index = 5u;
|
||||
if (out->table_a_present) {
|
||||
out->table_a_dimension = low16(lanes[index]);
|
||||
out->table_a_option = low16(lanes[index + 1u]);
|
||||
out->table_a_extra = low16(lanes[index + 2u]);
|
||||
index += 5u;
|
||||
} else {
|
||||
out->table_a_dimension = 77;
|
||||
}
|
||||
if (out->table_b_present) {
|
||||
if (!out->table_a_present) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT,
|
||||
"Rosella rp table B cannot be present without table A");
|
||||
}
|
||||
out->table_b_dimension = low16(lanes[index]);
|
||||
out->table_b_extra = low16(lanes[index + 1u]);
|
||||
out->table_b_groups = low16(lanes[index + 2u]);
|
||||
index += 3u;
|
||||
}
|
||||
if (out->table_c_present) {
|
||||
out->table_c_dimension = low16(lanes[index]);
|
||||
index += 1u;
|
||||
}
|
||||
const long long payload_words =
|
||||
static_cast<long long>(index) - 2 +
|
||||
(out->table_b_present ? (out->table_b_dimension + 380 * out->table_b_groups +
|
||||
out->table_b_extra + 79)
|
||||
: 0) +
|
||||
(out->table_a_present ? (171 * out->table_a_extra + 79 +
|
||||
2 * (out->table_a_option + 14 * out->table_a_dimension))
|
||||
: 0) +
|
||||
11 + (out->table_c_present ? (314 * out->table_c_dimension + 1) : 0);
|
||||
if (payload_words < 0) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT, "malformed Rosella rp header");
|
||||
}
|
||||
out->active_lanes = static_cast<std::size_t>(2 + payload_words);
|
||||
if (lanes.size() < out->active_lanes) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT, "Rosella rp is truncated");
|
||||
}
|
||||
std::uint32_t computed = 0xA569u;
|
||||
for (std::size_t lane = 2u; lane < out->active_lanes; ++lane) {
|
||||
computed ^= low16(lanes[lane]);
|
||||
}
|
||||
out->computed_checksum = static_cast<std::uint16_t>(computed & 0xFFFFu);
|
||||
out->checksum_valid = out->computed_checksum == out->stored_checksum;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
// _unpack_field: the serialized 154-per-direction field lanes to the padded grid.
|
||||
void unpack_field(const std::int32_t* serialized, int directions, int exponent,
|
||||
std::vector<float>* padded) {
|
||||
padded->assign(static_cast<std::size_t>(160 * directions), 0.0f);
|
||||
const int stride8 = 8 * directions;
|
||||
const int stride2 = 2 * directions;
|
||||
for (int source = 0; source < 154 * directions; ++source) {
|
||||
const int group4 = (source % stride8) / stride2;
|
||||
const int destination = (group4 & 3) + 4 * (source % stride2 +
|
||||
2 * directions * (source / stride8 +
|
||||
(group4 >> 2)));
|
||||
(*padded)[static_cast<std::size_t>(destination)] =
|
||||
q15_exp(serialized[source], exponent);
|
||||
}
|
||||
}
|
||||
|
||||
// _unpack_table_a_grid: the serialized table-A rows to the padded lane grid.
|
||||
void unpack_table_a_grid(const std::int32_t* serialized, int dimension, int serialized_rows,
|
||||
int padded_rows, int lane_group, std::vector<float>* padded) {
|
||||
padded->assign(static_cast<std::size_t>(padded_rows) * static_cast<std::size_t>(dimension),
|
||||
0.0f);
|
||||
const int group_width = lane_group * 4;
|
||||
for (int source = 0; source < serialized_rows * dimension; ++source) {
|
||||
const int remainder = source % group_width;
|
||||
const int destination = (remainder / lane_group) +
|
||||
4 * (remainder % lane_group +
|
||||
group_width / 4 * (source / group_width));
|
||||
(*padded)[static_cast<std::size_t>(destination)] = q15(serialized[source]);
|
||||
}
|
||||
}
|
||||
|
||||
void unpack_table_a_extra(const std::int32_t* serialized, std::vector<float>* padded) {
|
||||
padded->assign(160u, 0.0f);
|
||||
for (int source = 0; source < 154; ++source) {
|
||||
const int remainder = source & 7;
|
||||
const int destination = (remainder >> 1) + 4 * ((source & 1) + 2 * (source >> 3));
|
||||
(*padded)[static_cast<std::size_t>(destination)] = q15(serialized[source]);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string RosellaModel::summary() const {
|
||||
std::string name = capture.name.empty() ? std::string("unnamed") : capture.name;
|
||||
return "Rosella personalized_headphone '" + name + "' (" +
|
||||
(room_model.empty() ? std::string("unknown room") : room_model) + "), " +
|
||||
std::to_string(table_a_dimension) + " HQMF / 77 hybrid @ " +
|
||||
std::to_string(sample_rate) + " Hz";
|
||||
}
|
||||
|
||||
Status load_personalized_headphone(const std::string& path, RosellaModel* out) {
|
||||
if (out == nullptr) {
|
||||
return model_fail(JOC_ERR_INVALID_ARGUMENT, "null Rosella model destination");
|
||||
}
|
||||
if (!fs_utf8::exists(path)) {
|
||||
return model_fail(JOC_ERR_HRTF_NOT_FOUND, "personalized headphone model not found: " + path);
|
||||
}
|
||||
std::ifstream stream = fs_utf8::open_input(path);
|
||||
if (!stream.good()) {
|
||||
return model_fail(JOC_ERR_IO, "cannot open " + path);
|
||||
}
|
||||
std::string text((std::istreambuf_iterator<char>(stream)), std::istreambuf_iterator<char>());
|
||||
if (text.empty()) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT, "empty personalized headphone model: " + path);
|
||||
}
|
||||
// The checksum is taken over the coefficient lanes, exactly as upstream hashes
|
||||
// the int32 image of the array.
|
||||
const std::size_t first = text.find_first_not_of(" \t\r\n");
|
||||
if (first == std::string::npos || text[first] != '{') {
|
||||
return model_fail(JOC_ERR_NOT_SUPPORTED,
|
||||
"raw rp models are not supported; use a .personalized_headphone JSON");
|
||||
}
|
||||
std::vector<json::Member> root;
|
||||
std::string error;
|
||||
if (!json::parse_object(text, &root, &error)) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT, "invalid personalized headphone JSON: " + error);
|
||||
}
|
||||
const json::Member* personalized = json::find(root, "personalized_hrtf");
|
||||
if (personalized == nullptr) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT, "personalized_hrtf is missing");
|
||||
}
|
||||
std::vector<json::Member> inner;
|
||||
if (!json::parse_object(personalized->raw, &inner, &error)) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT, "invalid personalized_hrtf object: " + error);
|
||||
}
|
||||
const json::Member* virtualizer = json::find(inner, "virtualizer_parameters");
|
||||
if (virtualizer == nullptr) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT, "virtualizer_parameters is missing");
|
||||
}
|
||||
std::vector<json::Member> parameters;
|
||||
if (!json::parse_object(virtualizer->raw, ¶meters, &error)) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT, "invalid virtualizer_parameters: " + error);
|
||||
}
|
||||
const json::Member* coefficient_member = json::find(parameters, "rosella_coefficients");
|
||||
if (coefficient_member == nullptr) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT, "rosella_coefficients is missing");
|
||||
}
|
||||
RosellaModel model;
|
||||
model.source_path = path;
|
||||
if (const json::Member* member = json::find(parameters, "rosella_coefficients_version")) {
|
||||
json::as_string(*member, &model.coefficient_version);
|
||||
}
|
||||
if (const json::Member* member = json::find(parameters, "room_model")) {
|
||||
json::as_string(*member, &model.room_model);
|
||||
}
|
||||
if (const json::Member* capture = json::find(inner, "phrtf_capture_metadata")) {
|
||||
std::vector<json::Member> fields;
|
||||
if (json::parse_object(capture->raw, &fields, &error)) {
|
||||
const std::pair<const char*, std::string*> mapping[] = {
|
||||
{"capture_submission_date", &model.capture.capture_submission_date},
|
||||
{"capture_type", &model.capture.capture_type},
|
||||
{"label", &model.capture.label},
|
||||
{"name", &model.capture.name},
|
||||
{"phrtf_algorithm_version", &model.capture.algorithm_version},
|
||||
{"phrtf_creation_date", &model.capture.creation_date},
|
||||
{"uuid", &model.capture.uuid},
|
||||
{"version", &model.capture.version},
|
||||
};
|
||||
for (const auto& entry : mapping) {
|
||||
if (const json::Member* member = json::find(fields, entry.first)) {
|
||||
json::as_string(*member, entry.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<std::int32_t> lanes;
|
||||
if (!parse_int_array(coefficient_member->raw, &lanes, &error)) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT, error);
|
||||
}
|
||||
{
|
||||
crypto::Sha256 hash;
|
||||
hash.update(lanes.data(), lanes.size() * sizeof(std::int32_t));
|
||||
model.coefficient_sha256 = hash.finish_hex();
|
||||
}
|
||||
|
||||
RpHeader header;
|
||||
Status status = inspect_rp(lanes, &header);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (!header.checksum_valid || header.active_lanes != lanes.size()) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT,
|
||||
"invalid or non-active Rosella rp coefficient sequence");
|
||||
}
|
||||
if (!header.table_a_present || !header.table_b_present || header.table_c_present) {
|
||||
return model_fail(JOC_ERR_NOT_SUPPORTED,
|
||||
"the renderer requires table A+B and no table C");
|
||||
}
|
||||
if (header.table_a_dimension != 64 || header.table_a_option != 3) {
|
||||
return model_fail(JOC_ERR_NOT_SUPPORTED,
|
||||
"the renderer requires the observed 64-channel HQMF layout");
|
||||
}
|
||||
if (header.table_b_dimension != 20 || header.table_b_groups != 36) {
|
||||
return model_fail(JOC_ERR_NOT_SUPPORTED,
|
||||
"the renderer requires 20 hybrid groups and 36 direction terms");
|
||||
}
|
||||
|
||||
const std::int32_t* values = lanes.data();
|
||||
const std::size_t total = lanes.size();
|
||||
std::size_t position = 13u;
|
||||
const int extra = header.table_a_extra;
|
||||
model.table_a_dimension = header.table_a_dimension;
|
||||
model.table_a_option = header.table_a_option;
|
||||
model.table_a_extra = extra;
|
||||
model.table_a_header_field = low16(values[8]);
|
||||
model.table_a_header_25 = low16(values[9]);
|
||||
model.table_a_control = low16(values[position]);
|
||||
model.field_exponent = values[position];
|
||||
position += 1u;
|
||||
const int option_count = header.table_a_option;
|
||||
model.table_a_option_ids.resize(static_cast<std::size_t>(option_count));
|
||||
for (int index = 0; index < option_count; ++index) {
|
||||
model.table_a_option_ids[static_cast<std::size_t>(index)] =
|
||||
low16(values[position + static_cast<std::size_t>(index)]);
|
||||
}
|
||||
position += static_cast<std::size_t>(option_count);
|
||||
model.table_a_option_values.resize(static_cast<std::size_t>(option_count));
|
||||
for (int index = 0; index < option_count; ++index) {
|
||||
model.table_a_option_values[static_cast<std::size_t>(index)] =
|
||||
q15(values[position + static_cast<std::size_t>(index)]);
|
||||
}
|
||||
position += static_cast<std::size_t>(option_count);
|
||||
model.table_a_scalar = q15(values[position]);
|
||||
position += 1u;
|
||||
const int dimension = header.table_a_dimension;
|
||||
unpack_table_a_grid(values + position, dimension, 16, 20, 16,
|
||||
&model.table_a_filter_16x64_padded);
|
||||
position += static_cast<std::size_t>(16 * dimension);
|
||||
for (int index = 0; index < 4; ++index) {
|
||||
model.table_a_four_integers[static_cast<std::size_t>(index)] =
|
||||
low16(values[position + static_cast<std::size_t>(index)]);
|
||||
}
|
||||
position += 4u;
|
||||
model.table_a_integer = low16(values[position]);
|
||||
position += 1u;
|
||||
unpack_table_a_grid(values + position, dimension, 8, 10, 8,
|
||||
&model.table_a_filter_8x64_padded);
|
||||
position += static_cast<std::size_t>(8 * dimension);
|
||||
model.table_a_vector16.resize(16u);
|
||||
for (int index = 0; index < 16; ++index) {
|
||||
model.table_a_vector16[static_cast<std::size_t>(index)] =
|
||||
q15(values[position + static_cast<std::size_t>(index)]);
|
||||
}
|
||||
position += 16u;
|
||||
unpack_table_a_grid(values + position, dimension, 4, 5, 4,
|
||||
&model.table_a_filter_4x64_padded);
|
||||
position += static_cast<std::size_t>(4 * dimension);
|
||||
model.table_a_extra_indices.resize(static_cast<std::size_t>(extra));
|
||||
for (int index = 0; index < extra; ++index) {
|
||||
model.table_a_extra_indices[static_cast<std::size_t>(index)] =
|
||||
low16(values[position + static_cast<std::size_t>(index)]);
|
||||
}
|
||||
position += static_cast<std::size_t>(extra);
|
||||
model.table_a_extra_fields_padded.assign(static_cast<std::size_t>(extra) * 160u, 0.0f);
|
||||
std::vector<float> unpacked;
|
||||
for (int index = 0; index < extra; ++index) {
|
||||
unpack_table_a_extra(values + position, &unpacked);
|
||||
std::copy(unpacked.begin(), unpacked.end(),
|
||||
model.table_a_extra_fields_padded.begin() + static_cast<std::ptrdiff_t>(index) * 160);
|
||||
position += 154u;
|
||||
}
|
||||
model.table_a_extra_vectors.assign(static_cast<std::size_t>(extra) * 16u, 0.0f);
|
||||
for (int index = 0; index < extra; ++index) {
|
||||
for (int lane = 0; lane < 16; ++lane) {
|
||||
model.table_a_extra_vectors[static_cast<std::size_t>(index) * 16u +
|
||||
static_cast<std::size_t>(lane)] =
|
||||
q15(values[position + static_cast<std::size_t>(lane)]);
|
||||
}
|
||||
position += 16u;
|
||||
}
|
||||
const std::size_t table_b_start = position;
|
||||
if (table_b_start != 13u + 1821u + static_cast<std::size_t>(171 * extra)) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT, "Rosella table-A parser lost its place");
|
||||
}
|
||||
|
||||
model.sample_rate = 2 * low16(values[position]);
|
||||
position += 1u;
|
||||
model.matrix_exponent = values[position];
|
||||
position += 1u;
|
||||
const std::size_t matrix_count = 36u * 36u;
|
||||
model.matrix_left.resize(matrix_count);
|
||||
model.matrix_right.resize(matrix_count);
|
||||
for (std::size_t index = 0; index < matrix_count; ++index) {
|
||||
model.matrix_left[index] = q15_exp(values[position + index], model.matrix_exponent);
|
||||
}
|
||||
position += matrix_count;
|
||||
for (std::size_t index = 0; index < matrix_count; ++index) {
|
||||
model.matrix_right[index] = q15_exp(values[position + index], model.matrix_exponent);
|
||||
}
|
||||
position += matrix_count;
|
||||
model.vector_left.resize(36u);
|
||||
model.vector_right.resize(36u);
|
||||
for (int index = 0; index < 36; ++index) {
|
||||
model.vector_left[static_cast<std::size_t>(index)] =
|
||||
q15_exp(values[position + static_cast<std::size_t>(index)], model.matrix_exponent);
|
||||
}
|
||||
position += 36u;
|
||||
for (int index = 0; index < 36; ++index) {
|
||||
model.vector_right[static_cast<std::size_t>(index)] =
|
||||
q15_exp(values[position + static_cast<std::size_t>(index)], model.matrix_exponent);
|
||||
}
|
||||
position += 36u;
|
||||
|
||||
const std::size_t serialized_count = 154u * 36u;
|
||||
unpack_field(values + position, 36, model.field_exponent, &model.field_left_padded);
|
||||
bool odd_zero = true;
|
||||
for (std::size_t index = 1u; index < serialized_count; index += 2u) {
|
||||
const float value = q15_exp(values[position + index], model.field_exponent);
|
||||
if (std::abs(value) > 1.0e-6f) {
|
||||
odd_zero = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
model.field_left_odd_serialized_zero = odd_zero;
|
||||
position += serialized_count;
|
||||
unpack_field(values + position, 36, model.field_exponent, &model.field_right_padded);
|
||||
position += serialized_count;
|
||||
|
||||
model.hybrid_flags.resize(20u);
|
||||
int active_hybrid = 0;
|
||||
for (int index = 0; index < 20; ++index) {
|
||||
model.hybrid_flags[static_cast<std::size_t>(index)] =
|
||||
low16(values[position + static_cast<std::size_t>(index)]);
|
||||
if (model.hybrid_flags[static_cast<std::size_t>(index)] == 1) {
|
||||
++active_hybrid;
|
||||
}
|
||||
}
|
||||
position += 20u;
|
||||
if (active_hybrid != header.table_b_extra) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT, "hybrid value count does not match the header");
|
||||
}
|
||||
model.hybrid_values.resize(static_cast<std::size_t>(active_hybrid));
|
||||
for (int index = 0; index < active_hybrid; ++index) {
|
||||
model.hybrid_values[static_cast<std::size_t>(index)] =
|
||||
q15(values[position + static_cast<std::size_t>(index)]);
|
||||
}
|
||||
position += static_cast<std::size_t>(active_hybrid);
|
||||
model.model_scalars.resize(5u);
|
||||
for (int index = 0; index < 5; ++index) {
|
||||
model.model_scalars[static_cast<std::size_t>(index)] =
|
||||
q15(values[position + static_cast<std::size_t>(index)]);
|
||||
}
|
||||
position += 5u;
|
||||
const std::size_t expected_tail =
|
||||
table_b_start + static_cast<std::size_t>(header.table_b_dimension +
|
||||
380 * header.table_b_groups +
|
||||
header.table_b_extra + 79);
|
||||
if (position != expected_tail) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT, "Rosella table-B parser lost its place");
|
||||
}
|
||||
|
||||
model.header_float_scalars[0] = q15(values[position]);
|
||||
model.header_float_scalars[1] = q15(values[position + 1u]) * 16.0f;
|
||||
model.header_integer_fields[0] = values[position + 2u];
|
||||
model.header_integer_fields[1] = low16(values[position + 3u]);
|
||||
position += 4u;
|
||||
for (int profile = 0; profile < 4; ++profile) {
|
||||
RosellaDistanceProfile parsed;
|
||||
for (int index = 0; index < 6; ++index) {
|
||||
parsed.bounds[static_cast<std::size_t>(index)] =
|
||||
q15(values[position + static_cast<std::size_t>(index)]);
|
||||
}
|
||||
position += 6u;
|
||||
parsed.distance_scale_m =
|
||||
q15_exp(values[position], values[position + 1u]);
|
||||
position += 2u;
|
||||
parsed.inverse_distance_per_m = q15(values[position]);
|
||||
parsed.axis_scales_internal[0] = q15(values[position + 1u]);
|
||||
parsed.axis_scales_internal[1] = q15(values[position + 2u]);
|
||||
parsed.axis_scales_internal[2] = q15(values[position + 3u]);
|
||||
parsed.minimum_normalized_radius = q15(values[position + 4u]);
|
||||
position += 5u;
|
||||
model.profiles[static_cast<std::size_t>(profile)] = parsed;
|
||||
}
|
||||
model.profile_tail.resize(8u);
|
||||
for (int index = 0; index < 8; ++index) {
|
||||
model.profile_tail[static_cast<std::size_t>(index)] =
|
||||
q15(values[position + static_cast<std::size_t>(index)]);
|
||||
}
|
||||
position += 8u;
|
||||
for (int index = 0; index < 3; ++index) {
|
||||
model.post_fields[static_cast<std::size_t>(index)] =
|
||||
values[position + static_cast<std::size_t>(index)];
|
||||
}
|
||||
position += 3u;
|
||||
if (position != total) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT, "unparsed Rosella coefficient lanes");
|
||||
}
|
||||
if (model.sample_rate != 48000) {
|
||||
return model_fail(JOC_ERR_HRTF_FORMAT,
|
||||
"Rosella model sample rate must be 48000, got " +
|
||||
std::to_string(model.sample_rate));
|
||||
}
|
||||
*out = std::move(model);
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
} // namespace joc::hrtf
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "foundation/status.h"
|
||||
|
||||
// Parser for the Dolby ".personalized_headphone" model (upstream rosella_model.py).
|
||||
// The file is JSON whose virtualizer_parameters carry the raw "rp" coefficient
|
||||
// lanes; everything the renderer needs is unpacked here, in the same float32
|
||||
// arithmetic the reference uses, because those values are part of the model.
|
||||
namespace joc::hrtf {
|
||||
|
||||
struct RosellaDistanceProfile {
|
||||
std::array<float, 6> bounds{};
|
||||
float distance_scale_m = 0.0f;
|
||||
float inverse_distance_per_m = 0.0f;
|
||||
std::array<float, 3> axis_scales_internal{};
|
||||
float minimum_normalized_radius = 0.0f;
|
||||
};
|
||||
|
||||
struct RosellaCaptureMetadata {
|
||||
std::string capture_submission_date;
|
||||
std::string capture_type;
|
||||
std::string label;
|
||||
std::string name;
|
||||
std::string algorithm_version;
|
||||
std::string creation_date;
|
||||
std::string uuid;
|
||||
std::string version;
|
||||
};
|
||||
|
||||
struct RosellaModel {
|
||||
std::string source_path;
|
||||
std::string coefficient_sha256;
|
||||
std::string coefficient_version;
|
||||
std::string room_model;
|
||||
RosellaCaptureMetadata capture;
|
||||
|
||||
int table_a_dimension = 0;
|
||||
int table_a_option = 0;
|
||||
int table_a_extra = 0;
|
||||
int table_a_header_field = 0;
|
||||
int table_a_header_25 = 0;
|
||||
int table_a_control = 0;
|
||||
std::vector<int> table_a_option_ids;
|
||||
std::vector<float> table_a_option_values;
|
||||
float table_a_scalar = 0.0f;
|
||||
std::vector<float> table_a_filter_16x64_padded;
|
||||
std::array<int, 4> table_a_four_integers{};
|
||||
int table_a_integer = 0;
|
||||
std::vector<float> table_a_filter_8x64_padded;
|
||||
std::vector<float> table_a_vector16;
|
||||
std::vector<float> table_a_filter_4x64_padded;
|
||||
std::vector<int> table_a_extra_indices;
|
||||
std::vector<float> table_a_extra_fields_padded;
|
||||
std::vector<float> table_a_extra_vectors;
|
||||
|
||||
int sample_rate = 0;
|
||||
int matrix_exponent = 0;
|
||||
int field_exponent = 0;
|
||||
std::vector<float> matrix_left;
|
||||
std::vector<float> matrix_right;
|
||||
std::vector<float> vector_left;
|
||||
std::vector<float> vector_right;
|
||||
std::vector<float> field_left_padded;
|
||||
std::vector<float> field_right_padded;
|
||||
bool field_left_odd_serialized_zero = false;
|
||||
std::vector<int> hybrid_flags;
|
||||
std::vector<float> hybrid_values;
|
||||
std::vector<float> model_scalars;
|
||||
std::array<float, 2> header_float_scalars{};
|
||||
std::array<int, 2> header_integer_fields{};
|
||||
std::array<RosellaDistanceProfile, 4> profiles{};
|
||||
std::vector<float> profile_tail;
|
||||
std::array<int, 3> post_fields{};
|
||||
|
||||
// One line for reports and logs: the capture name and room model are the
|
||||
// model's own strings, followed by the table layout and sample rate, e.g.
|
||||
// "Rosella personalized_headphone '<name>' (<room>), <N> HQMF / 77 hybrid @ <rate> Hz".
|
||||
std::string summary() const;
|
||||
};
|
||||
|
||||
Status load_personalized_headphone(const std::string& path, RosellaModel* out);
|
||||
|
||||
} // namespace joc::hrtf
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "foundation/status.h"
|
||||
#include "hrtf/rosella_model.h"
|
||||
#include "oamd/oamd_parser.h"
|
||||
#include "timeline/position_timeline.h"
|
||||
|
||||
// Rosella ".personalized_headphone" binaural renderer (upstream rosella_core.py,
|
||||
// rosella_direct.py, rosella_room.py and rosella_binaural_renderer.py). It takes
|
||||
// the same pipeline slot as the SOFA runtime: sixteen object channels per frame in,
|
||||
// interleaved stereo out, with the OAMD timeline driving the per-block parameters.
|
||||
namespace joc::hrtf {
|
||||
|
||||
// rosella_direct.BINAURAL_PROFILE_NAMES.
|
||||
enum class RosellaProfile : std::int32_t { Near = 1, Far = 2, Mid = 3 };
|
||||
|
||||
struct RosellaRenderOptions {
|
||||
RosellaProfile profile = RosellaProfile::Mid;
|
||||
std::int64_t object_delay_samples = 1473;
|
||||
double tail_seconds = 5.0;
|
||||
double output_gain = 1.0;
|
||||
int chunk_frames = 64;
|
||||
int room_impulse_slots = 4096;
|
||||
};
|
||||
|
||||
class RosellaRuntime {
|
||||
public:
|
||||
RosellaRuntime();
|
||||
~RosellaRuntime();
|
||||
RosellaRuntime(const RosellaRuntime&) = delete;
|
||||
RosellaRuntime& operator=(const RosellaRuntime&) = delete;
|
||||
|
||||
Status open(const RosellaModel& model, const RosellaRenderOptions& options);
|
||||
|
||||
// objects16_planar is channel-major: channel * 1536 + sample.
|
||||
Status submit_frame(const float* objects16_planar, const oamd::OamdUpdate* update,
|
||||
std::int64_t frame_index, std::int64_t outer_sample_offset,
|
||||
std::int64_t object_delay_samples);
|
||||
|
||||
// Drains the flush tail: the pending partial chunk plus flush_samples of silence.
|
||||
Status finish(std::uint32_t flush_samples, std::vector<double>* out);
|
||||
std::uint32_t finish_capacity(double tail_seconds) const;
|
||||
|
||||
Status reset();
|
||||
|
||||
const std::vector<double>& output() const;
|
||||
void take_output(std::vector<double>* out);
|
||||
|
||||
std::uint64_t input_samples() const;
|
||||
std::uint64_t processed_input_samples() const;
|
||||
std::uint64_t metadata_block_updates() const;
|
||||
const timeline::OamdPositionTimeline& timeline() const;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace joc::hrtf
|
||||
@@ -0,0 +1,255 @@
|
||||
#include "hrtf/sofa.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <utility>
|
||||
|
||||
#include "foundation/fs_utf8.h"
|
||||
#include "foundation/sha256.h"
|
||||
#include "io/hdf5.h"
|
||||
|
||||
namespace joc::hrtf {
|
||||
|
||||
namespace {
|
||||
|
||||
Status sofa_fail(joc_error code, const std::string& message) {
|
||||
return Status::fail(code, stage::kRender, message);
|
||||
}
|
||||
|
||||
std::string format_number(double value) {
|
||||
if (std::isfinite(value) && value == std::floor(value) && std::fabs(value) < 1.0e15) {
|
||||
return std::to_string(static_cast<long long>(value));
|
||||
}
|
||||
char buffer[32];
|
||||
std::snprintf(buffer, sizeof(buffer), "%.6g", value);
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
// Every array is checked against the element count the convention prescribes, so
|
||||
// a file whose shape disagrees with its metadata is rejected instead of silently
|
||||
// producing a shifted impulse response.
|
||||
Status read_doubles(const io::Hdf5File& file, const std::string& path, std::uint64_t expected,
|
||||
std::vector<double>* out) {
|
||||
if (!file.has_dataset(path)) {
|
||||
return sofa_fail(JOC_ERR_HRTF_FORMAT, "SOFA file has no " + path + " dataset");
|
||||
}
|
||||
const Status status = file.read_dataset_double(path, out);
|
||||
if (!status.ok()) {
|
||||
return sofa_fail(JOC_ERR_HRTF_FORMAT, "SOFA dataset " + path + ": " + status.message());
|
||||
}
|
||||
if (out->size() != expected) {
|
||||
return sofa_fail(JOC_ERR_HRTF_FORMAT,
|
||||
"SOFA dataset " + path + " holds " + std::to_string(out->size()) +
|
||||
" values, expected " + std::to_string(expected));
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status read_text(const io::Hdf5File& file, const std::string& name, bool required,
|
||||
std::string* out) {
|
||||
io::Hdf5Attribute attribute;
|
||||
const Status status = file.attribute("", name, &attribute);
|
||||
if (!status.ok()) {
|
||||
if (required) {
|
||||
return sofa_fail(JOC_ERR_HRTF_FORMAT,
|
||||
"SOFA file has no root attribute " + name + ": " + status.message());
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
*out = attribute.text;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
// SHA-256 of the whole file: the compiled-cache key is derived from it, so the
|
||||
// digest is taken over the exact bytes the parse consumed.
|
||||
std::string file_digest(const std::string& path) {
|
||||
std::ifstream stream = fs_utf8::open_input(path);
|
||||
if (!stream.good()) {
|
||||
return std::string();
|
||||
}
|
||||
crypto::Sha256 hash;
|
||||
std::vector<char> buffer(1u << 20);
|
||||
while (stream.good()) {
|
||||
stream.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
|
||||
const std::streamsize count = stream.gcount();
|
||||
if (count > 0) {
|
||||
hash.update(buffer.data(), static_cast<std::size_t>(count));
|
||||
}
|
||||
}
|
||||
return hash.finish_hex();
|
||||
}
|
||||
|
||||
// The coordinate declaration of one dataset, when the file carries it.
|
||||
void read_coordinates(const io::Hdf5File& file, const std::string& dataset,
|
||||
SofaCoordinate* out) {
|
||||
io::Hdf5Attribute attribute;
|
||||
if (file.attribute(dataset, "Type", &attribute).ok()) {
|
||||
out->type = attribute.text;
|
||||
}
|
||||
if (file.attribute(dataset, "Units", &attribute).ok()) {
|
||||
out->units = attribute.text;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string SofaHrir::summary() const {
|
||||
return "SOFA " + sofa_conventions + ", " + std::to_string(ir_count) + " IRs x " +
|
||||
std::to_string(ir_length) + " taps @ " + format_number(sample_rate) + " Hz";
|
||||
}
|
||||
|
||||
Status load_sofa(const std::string& path, SofaHrir* out) {
|
||||
if (out == nullptr) {
|
||||
return sofa_fail(JOC_ERR_INVALID_ARGUMENT, "null SOFA destination");
|
||||
}
|
||||
if (!fs_utf8::exists(path)) {
|
||||
return sofa_fail(JOC_ERR_HRTF_NOT_FOUND, "SOFA file not found: " + path);
|
||||
}
|
||||
io::Hdf5File file;
|
||||
Status status = file.open(path);
|
||||
if (!status.ok()) {
|
||||
return sofa_fail(JOC_ERR_HRTF_FORMAT, "SOFA file " + path + ": " + status.message());
|
||||
}
|
||||
|
||||
SofaHrir sofa;
|
||||
sofa.source_path = path;
|
||||
sofa.source_sha256 = file_digest(path);
|
||||
for (char& character : sofa.source_sha256) {
|
||||
character = static_cast<char>(std::toupper(static_cast<unsigned char>(character)));
|
||||
}
|
||||
status = read_text(file, "Conventions", true, &sofa.conventions);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
status = read_text(file, "SOFAConventions", true, &sofa.sofa_conventions);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (sofa.conventions != "SOFA" || sofa.sofa_conventions != "SimpleFreeFieldHRIR") {
|
||||
return sofa_fail(JOC_ERR_HRTF_UNSUPPORTED_CONVENTION,
|
||||
"SOFA conventions " + sofa.conventions + "/" + sofa.sofa_conventions +
|
||||
" are not SimpleFreeFieldHRIR");
|
||||
}
|
||||
status = read_text(file, "SOFAConventionsVersion", false, &sofa.convention_version);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
status = read_text(file, "Version", false, &sofa.version);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
status = read_text(file, "DataType", false, &sofa.data_type);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
status = read_text(file, "RoomType", false, &sofa.room_type);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
status = read_text(file, "Title", false, &sofa.title);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
status = read_text(file, "DatabaseName", false, &sofa.database_name);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
status = read_text(file, "ListenerShortName", false, &sofa.listener_short_name);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
status = read_text(file, "Comment", false, &sofa.comment);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
io::Hdf5DatasetInfo info;
|
||||
status = file.dataset_info("Data.IR", &info);
|
||||
if (!status.ok()) {
|
||||
return sofa_fail(JOC_ERR_HRTF_FORMAT,
|
||||
"SOFA file has no usable Data.IR dataset: " + status.message());
|
||||
}
|
||||
if (info.shape.size() != 3u || info.shape[1] != 2u || info.shape[0] == 0u ||
|
||||
info.shape[2] == 0u) {
|
||||
return sofa_fail(JOC_ERR_HRTF_FORMAT, "SOFA Data.IR is not shaped (M, 2, N)");
|
||||
}
|
||||
if (info.shape[0] > 0xFFFFFFFFull || info.shape[2] > 0xFFFFFFFFull) {
|
||||
return sofa_fail(JOC_ERR_HRTF_FORMAT, "SOFA Data.IR is larger than this reader accepts");
|
||||
}
|
||||
sofa.ir_count = static_cast<std::uint32_t>(info.shape[0]);
|
||||
sofa.ir_length = static_cast<std::uint32_t>(info.shape[2]);
|
||||
const std::uint64_t taps = static_cast<std::uint64_t>(sofa.ir_count) * 2u * sofa.ir_length;
|
||||
status = read_doubles(file, "Data.IR", taps, &sofa.ir);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
std::vector<double> scalar;
|
||||
status = read_doubles(file, "Data.SamplingRate", 1u, &scalar);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
sofa.sample_rate = scalar[0];
|
||||
io::Hdf5Attribute attribute;
|
||||
if (file.attribute("Data.SamplingRate", "Units", &attribute).ok()) {
|
||||
sofa.sampling_rate_units = attribute.text;
|
||||
}
|
||||
|
||||
// Data.Delay is optional in the wild; absent means "no delay was measured".
|
||||
if (file.has_dataset("Data.Delay")) {
|
||||
std::vector<double> delay;
|
||||
status = read_doubles(file, "Data.Delay", 2u, &delay);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
sofa.delay[0] = delay[0];
|
||||
sofa.delay[1] = delay[1];
|
||||
}
|
||||
|
||||
const std::uint64_t measurements = sofa.ir_count;
|
||||
status = read_doubles(file, "SourcePosition", measurements * 3u, &sofa.source_position);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
read_coordinates(file, "SourcePosition", &sofa.source_position_coordinates);
|
||||
|
||||
std::vector<double> vector;
|
||||
status = read_doubles(file, "ListenerPosition", 3u, &vector);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
std::copy(vector.begin(), vector.end(), sofa.listener_position);
|
||||
read_coordinates(file, "ListenerPosition", &sofa.listener_position_coordinates);
|
||||
status = read_doubles(file, "ListenerView", 3u, &vector);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
std::copy(vector.begin(), vector.end(), sofa.listener_view);
|
||||
read_coordinates(file, "ListenerView", &sofa.listener_view_coordinates);
|
||||
status = read_doubles(file, "ListenerUp", 3u, &vector);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
std::copy(vector.begin(), vector.end(), sofa.listener_up);
|
||||
read_coordinates(file, "ListenerUp", &sofa.listener_up_coordinates);
|
||||
status = read_doubles(file, "EmitterPosition", 3u, &vector);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
std::copy(vector.begin(), vector.end(), sofa.emitter_position);
|
||||
read_coordinates(file, "EmitterPosition", &sofa.emitter_position_coordinates);
|
||||
status = read_doubles(file, "ReceiverPosition", 6u, &vector);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
std::copy(vector.begin(), vector.end(), sofa.receiver_position);
|
||||
read_coordinates(file, "ReceiverPosition", &sofa.receiver_position_coordinates);
|
||||
|
||||
*out = std::move(sofa);
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
} // namespace joc::hrtf
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "foundation/status.h"
|
||||
|
||||
namespace joc::hrtf {
|
||||
|
||||
// Coordinate declaration of one SOFA variable: Type ("spherical"/"cartesian") and
|
||||
// Units. Empty when the file does not declare them (ListenerUp inherits).
|
||||
struct SofaCoordinate {
|
||||
std::string type;
|
||||
std::string units;
|
||||
};
|
||||
|
||||
// SOFA SimpleFreeFieldHRIR as this project consumes it: the impulse responses,
|
||||
// the measurement geometry and the metadata needed to report what was loaded.
|
||||
// All angles are degrees, all distances metres, exactly as the file stores them.
|
||||
struct SofaHrir {
|
||||
double sample_rate = 0.0;
|
||||
std::uint32_t ir_count = 0; // M: number of measurements
|
||||
std::uint32_t ir_length = 0; // N: taps per impulse response
|
||||
std::vector<double> ir; // C order [M][2][N]
|
||||
double delay[2] = {0.0, 0.0};
|
||||
std::vector<double> source_position; // M*3
|
||||
double listener_position[3] = {0.0, 0.0, 0.0};
|
||||
double listener_view[3] = {1.0, 0.0, 0.0};
|
||||
double listener_up[3] = {0.0, 0.0, 1.0};
|
||||
double emitter_position[3] = {0.0, 0.0, 0.0};
|
||||
double receiver_position[6] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
|
||||
std::string conventions;
|
||||
std::string sofa_conventions;
|
||||
std::string convention_version;
|
||||
std::string version;
|
||||
std::string data_type;
|
||||
std::string room_type;
|
||||
std::string title;
|
||||
std::string database_name;
|
||||
std::string listener_short_name;
|
||||
std::string comment;
|
||||
std::string sampling_rate_units;
|
||||
SofaCoordinate source_position_coordinates;
|
||||
SofaCoordinate listener_position_coordinates;
|
||||
SofaCoordinate listener_view_coordinates;
|
||||
SofaCoordinate listener_up_coordinates;
|
||||
SofaCoordinate emitter_position_coordinates;
|
||||
SofaCoordinate receiver_position_coordinates;
|
||||
// Identity of the file itself, needed for the compiled-cache key.
|
||||
std::string source_path;
|
||||
std::string source_sha256;
|
||||
|
||||
// One line for reports and logs:
|
||||
// "SOFA SimpleFreeFieldHRIR, <M> IRs x <N> taps @ <rate> Hz".
|
||||
std::string summary() const;
|
||||
};
|
||||
|
||||
Status load_sofa(const std::string& path, SofaHrir* out);
|
||||
|
||||
} // namespace joc::hrtf
|
||||
@@ -0,0 +1,237 @@
|
||||
#include "hrtf/sofa_cache.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "foundation/fs_utf8.h"
|
||||
#include "hrtf/sofa.h"
|
||||
|
||||
namespace joc::hrtf {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Small process-local cache: the reference keeps the last eight compiled fields.
|
||||
constexpr std::size_t kMemoryCacheEntries = 8;
|
||||
|
||||
struct MemoryEntry {
|
||||
std::string key;
|
||||
Field field;
|
||||
};
|
||||
|
||||
std::mutex& memory_mutex() {
|
||||
static std::mutex mutex;
|
||||
return mutex;
|
||||
}
|
||||
|
||||
std::list<MemoryEntry>& memory_cache() {
|
||||
static std::list<MemoryEntry> cache;
|
||||
return cache;
|
||||
}
|
||||
|
||||
bool memory_cache_get(const std::string& key, Field* out) {
|
||||
std::lock_guard<std::mutex> lock(memory_mutex());
|
||||
std::list<MemoryEntry>& cache = memory_cache();
|
||||
for (auto entry = cache.begin(); entry != cache.end(); ++entry) {
|
||||
if (entry->key == key) {
|
||||
*out = entry->field;
|
||||
cache.splice(cache.begin(), cache, entry);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void memory_cache_put(const std::string& key, const Field& field) {
|
||||
std::lock_guard<std::mutex> lock(memory_mutex());
|
||||
std::list<MemoryEntry>& cache = memory_cache();
|
||||
for (auto entry = cache.begin(); entry != cache.end(); ++entry) {
|
||||
if (entry->key == key) {
|
||||
entry->field = field;
|
||||
cache.splice(cache.begin(), cache, entry);
|
||||
return;
|
||||
}
|
||||
}
|
||||
cache.push_front(MemoryEntry{key, field});
|
||||
while (cache.size() > kMemoryCacheEntries) {
|
||||
cache.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
Status cache_fail(joc_error code, const std::string& message) {
|
||||
return Status::fail(code, stage::kRender, message);
|
||||
}
|
||||
|
||||
std::string upper(std::string text) {
|
||||
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char value) {
|
||||
return static_cast<char>(std::toupper(value));
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status parse_cache_policy(const std::string& text, CachePolicy* out) {
|
||||
if (out == nullptr) {
|
||||
return cache_fail(JOC_ERR_INVALID_ARGUMENT, "null cache policy");
|
||||
}
|
||||
if (text == "none") {
|
||||
*out = CachePolicy::None;
|
||||
return Status::success();
|
||||
}
|
||||
if (text == "memory") {
|
||||
*out = CachePolicy::Memory;
|
||||
return Status::success();
|
||||
}
|
||||
if (text == "disk") {
|
||||
*out = CachePolicy::Disk;
|
||||
return Status::success();
|
||||
}
|
||||
return cache_fail(JOC_ERR_INVALID_CONFIG, "cache_policy must be none, memory, or disk");
|
||||
}
|
||||
|
||||
Status validate_jochrtf(const std::string& path, const std::string& source_sha256,
|
||||
const std::string& cache_key, Field* out) {
|
||||
Field field;
|
||||
const Status status = load_jochrtf(path, &field);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (!source_sha256.empty() && field.source_sha256 != upper(source_sha256)) {
|
||||
return cache_fail(JOC_ERR_HRTF_HASH, "compiled HRTF source hash mismatch");
|
||||
}
|
||||
if (!cache_key.empty() && field.cache_key != upper(cache_key)) {
|
||||
return cache_fail(JOC_ERR_HRTF_HASH, "compiled HRTF configuration hash mismatch");
|
||||
}
|
||||
if (out != nullptr) {
|
||||
*out = std::move(field);
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status save_jochrtf_atomic(const Field& field, const std::string& path) {
|
||||
if (path.empty()) {
|
||||
return cache_fail(JOC_ERR_INVALID_ARGUMENT, "empty compiled HRTF cache path");
|
||||
}
|
||||
const fs::path target = fs_utf8::to_path(path);
|
||||
std::error_code error;
|
||||
if (target.has_parent_path()) {
|
||||
fs::create_directories(target.parent_path(), error);
|
||||
if (error) {
|
||||
return cache_fail(JOC_ERR_OUTPUT_OPEN,
|
||||
"cannot create " + fs_utf8::from_path(target.parent_path()));
|
||||
}
|
||||
}
|
||||
const std::string temporary = path + ".tmp";
|
||||
Status status = write_jochrtf(field, temporary);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
// The rename is what makes a half-written cache impossible to observe.
|
||||
fs::rename(fs_utf8::to_path(temporary), target, error);
|
||||
if (error) {
|
||||
fs::remove(fs_utf8::to_path(temporary), error);
|
||||
return cache_fail(JOC_ERR_OUTPUT_WRITE, "cannot replace " + path);
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status load_or_compile_sofa_field(const SofaFieldRequest& request, Field* out,
|
||||
std::string* cache_path) {
|
||||
if (out == nullptr) {
|
||||
return cache_fail(JOC_ERR_INVALID_ARGUMENT, "null compiled HRTF destination");
|
||||
}
|
||||
if (request.sofa_path.empty()) {
|
||||
return cache_fail(JOC_ERR_INVALID_ARGUMENT, "no SOFA path for the compiled HRTF field");
|
||||
}
|
||||
if (request.policy == CachePolicy::Disk && request.cache_dir.empty()) {
|
||||
return cache_fail(JOC_ERR_INVALID_CONFIG, "the disk cache policy needs a cache directory");
|
||||
}
|
||||
SofaHrir sofa;
|
||||
Status status = load_sofa(request.sofa_path, &sofa);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
CanonicalHrtf canonical;
|
||||
status = canonicalize_sofa(sofa, &canonical);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
// The key depends on the shell that the radius selects, exactly as upstream.
|
||||
double actual_radius = request.options.shell_radius_m;
|
||||
(void)canonical_shell_indices(canonical, request.options.shell_radius_m, &actual_radius);
|
||||
const std::string key = compiled_hrtf_cache_key(
|
||||
canonical.source_sha256, canonical.sample_rate_hz, actual_radius, request.options.order,
|
||||
request.options.projection_ridge, request.options.sh_ridge);
|
||||
if (cache_path != nullptr) {
|
||||
cache_path->clear();
|
||||
}
|
||||
|
||||
std::string target;
|
||||
if (request.policy == CachePolicy::Disk) {
|
||||
target = request.cache_dir;
|
||||
if (!target.empty() && target.back() != '/' && target.back() != '\\') {
|
||||
target += "/";
|
||||
}
|
||||
target += cache_file_name(canonical.source_path.empty()
|
||||
? std::string()
|
||||
: canonical.source_path,
|
||||
key);
|
||||
if (fs_utf8::exists(target)) {
|
||||
Field cached_field;
|
||||
const Status cached =
|
||||
validate_jochrtf(target, canonical.source_sha256, key, &cached_field);
|
||||
if (cached.ok()) {
|
||||
memory_cache_put(key, cached_field);
|
||||
*out = std::move(cached_field);
|
||||
if (cache_path != nullptr) {
|
||||
*cache_path = target;
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
}
|
||||
}
|
||||
Field field;
|
||||
if (request.policy != CachePolicy::None && memory_cache_get(key, &field)) {
|
||||
// A memory hit still materialises the disk cache the caller asked for.
|
||||
if (request.policy == CachePolicy::Disk) {
|
||||
status = save_jochrtf_atomic(field, target);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (cache_path != nullptr) {
|
||||
*cache_path = target;
|
||||
}
|
||||
}
|
||||
*out = std::move(field);
|
||||
return Status::success();
|
||||
}
|
||||
status = compile_canonical_field(canonical, request.options, &field);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (field.cache_key != key) {
|
||||
return cache_fail(JOC_ERR_INTERNAL, "internal compiled HRTF cache-key mismatch");
|
||||
}
|
||||
if (request.policy == CachePolicy::Disk) {
|
||||
status = save_jochrtf_atomic(field, target);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (cache_path != nullptr) {
|
||||
*cache_path = target;
|
||||
}
|
||||
}
|
||||
if (request.policy != CachePolicy::None) {
|
||||
memory_cache_put(key, field);
|
||||
}
|
||||
*out = std::move(field);
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
} // namespace joc::hrtf
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "foundation/status.h"
|
||||
#include "hrtf/jochrtf.h"
|
||||
#include "hrtf/sofa_field.h"
|
||||
|
||||
// Compiled-field cache: the .jochrtf is an internal artifact, so the caller only
|
||||
// names the SOFA file and the policy. "memory" keeps the compiled field in this
|
||||
// process, "disk" additionally reuses (and writes) <cache_dir>/<name>.<key>.jochrtf.
|
||||
namespace joc::hrtf {
|
||||
|
||||
enum class CachePolicy { None, Memory, Disk };
|
||||
|
||||
struct SofaFieldRequest {
|
||||
std::string sofa_path;
|
||||
CompileOptions options;
|
||||
CachePolicy policy = CachePolicy::Memory;
|
||||
std::string cache_dir; // required for the disk policy
|
||||
};
|
||||
|
||||
// Parses "none"/"memory"/"disk"; anything else is rejected.
|
||||
Status parse_cache_policy(const std::string& text, CachePolicy* out);
|
||||
|
||||
// Returns the compiled field, reusing a valid cache when the policy allows it.
|
||||
// `cache_path` (optional) receives the cache file that was read or written.
|
||||
Status load_or_compile_sofa_field(const SofaFieldRequest& request, Field* out,
|
||||
std::string* cache_path);
|
||||
|
||||
// Writes the field to `path` through a temporary file and an atomic rename.
|
||||
Status save_jochrtf_atomic(const Field& field, const std::string& path);
|
||||
|
||||
// Verifies that a cache file belongs to `source_sha256` and `cache_key`.
|
||||
Status validate_jochrtf(const std::string& path, const std::string& source_sha256,
|
||||
const std::string& cache_key, Field* out);
|
||||
|
||||
} // namespace joc::hrtf
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "foundation/status.h"
|
||||
#include "hrtf/jochrtf.h"
|
||||
#include "hrtf/sofa.h"
|
||||
|
||||
// SOFA SimpleFreeFieldHRIR -> compiled directional field, ported from the
|
||||
// reference chain (sofa_canonical.py + sofa_hrtf_field.py + the public
|
||||
// filterbank): the measurement shell is selected, one delay representation is
|
||||
// separated, the FIRs are projected onto the 64-QMF/77-hybrid filterbank and the
|
||||
// result is fitted with fifth-order ACN/N3D real spherical harmonics.
|
||||
namespace joc::hrtf {
|
||||
|
||||
inline constexpr int kFieldOrder = 5;
|
||||
inline constexpr int kFieldTerms = 36;
|
||||
inline constexpr double kFieldSampleRateHz = 48000.0;
|
||||
inline constexpr double kDefaultShellRadiusM = 1.0;
|
||||
inline constexpr double kDefaultProjectionRidge = 1.0e-3;
|
||||
inline constexpr double kDefaultSphericalHarmonicRidge = 1.0e-5;
|
||||
inline constexpr const char* kCompilerVersion = "joc-sofa-compiler-v1";
|
||||
inline constexpr const char* kPhasePolicyVersion = "sofa-delay-exactly-once-v1";
|
||||
inline constexpr const char* kShConvention = "ACN/N3D real";
|
||||
inline constexpr const char* kFilterbankTableVersion = "joc-public-64qmf-77hybrid-v1";
|
||||
// SHA-256 of the standard filterbank archive the embedded tables came from. It
|
||||
// participates in the cache key, so it is part of the file-format contract.
|
||||
inline constexpr const char* kFilterbankArchiveSha256 =
|
||||
"C05BEF4D26E96ECBD4694E2572F05DA400255C777BA5047300B9D3B1F81081CD";
|
||||
|
||||
struct CompileOptions {
|
||||
double shell_radius_m = kDefaultShellRadiusM;
|
||||
int order = kFieldOrder;
|
||||
double projection_ridge = kDefaultProjectionRidge;
|
||||
double sh_ridge = kDefaultSphericalHarmonicRidge;
|
||||
};
|
||||
|
||||
// Canonical HRIR set: Data.IR and Data.Delay stay separate, the listener frame is
|
||||
// applied to the source positions and the ears are ordered left/right.
|
||||
struct CanonicalHrtf {
|
||||
std::string source_path;
|
||||
std::string source_sha256;
|
||||
std::string convention;
|
||||
std::string convention_version;
|
||||
std::string processing_label;
|
||||
double sample_rate_hz = 0.0;
|
||||
std::uint32_t measurements = 0;
|
||||
std::uint32_t taps = 0;
|
||||
int left_receiver_index = 0;
|
||||
int right_receiver_index = 1;
|
||||
std::vector<double> source_position_cartesian_m; // [M,3] listener-local
|
||||
std::vector<double> unit_directions; // [M,3]
|
||||
std::vector<double> measurement_radius_m; // [M]
|
||||
std::vector<double> hrir; // [M,2,N] canonical L/R
|
||||
std::vector<double> delay_samples; // [M,2], not applied
|
||||
};
|
||||
|
||||
// Port of load_simple_free_field_hrir(): strict SimpleFreeFieldHRIR import.
|
||||
Status canonicalize_sofa(const SofaHrir& sofa, CanonicalHrtf* out);
|
||||
|
||||
// Compiles the canonical set into the runtime field (port of SofaHrtfField.fit).
|
||||
Status compile_sofa_field(const SofaHrir& sofa, const CompileOptions& options, Field* out);
|
||||
|
||||
// The measurements on the shell nearest to radius_m; actual_radius_m receives the
|
||||
// mean radius of that shell (upstream CanonicalHrtf.shell_indices).
|
||||
std::vector<std::size_t> canonical_shell_indices(const CanonicalHrtf& canonical, double radius_m,
|
||||
double* actual_radius_m);
|
||||
|
||||
// Compiles an already canonicalized set (used by tests and the cache layer).
|
||||
Status compile_canonical_field(const CanonicalHrtf& canonical, const CompileOptions& options,
|
||||
Field* out);
|
||||
|
||||
// Configuration hash that names the cache file (upstream compiled_hrtf_cache_key).
|
||||
std::string compiled_hrtf_cache_key(const std::string& source_sha256, double sample_rate_hz,
|
||||
double shell_radius_m, int order, double projection_ridge,
|
||||
double sh_ridge);
|
||||
|
||||
// Payload hash over the four arrays (upstream _payload_sha256).
|
||||
std::string field_payload_sha256(const Field& field);
|
||||
|
||||
// "<stem>.<first 20 key digits>.jochrtf", the upstream cache file name.
|
||||
std::string cache_file_name(const std::string& display_name, const std::string& cache_key);
|
||||
|
||||
// Serializes the field as a .jochrtf cache the upstream loader also accepts.
|
||||
Status write_jochrtf(const Field& field, const std::string& path);
|
||||
|
||||
// The analysis/gain/synthesis dictionary the projection solves against (dev check).
|
||||
std::vector<double> hybrid_gain_synthesis_dictionary_for_check(std::size_t sample_count);
|
||||
|
||||
// Shell directions and their spherical Voronoi weights (dev check).
|
||||
void shell_directions_and_weights_for_check(const SofaHrir& sofa, double radius_m,
|
||||
std::vector<double>* directions,
|
||||
std::vector<double>* weights);
|
||||
|
||||
// PublicAnalysis77 on a unit impulse, interleaved complex (dev check).
|
||||
std::vector<double> analysis_impulse_for_check(std::size_t total_samples);
|
||||
|
||||
// The 77 hybrid-band centre frequencies at 48 kHz.
|
||||
const std::vector<double>& hybrid_band_center_frequencies_hz();
|
||||
|
||||
} // namespace joc::hrtf
|
||||
@@ -0,0 +1,241 @@
|
||||
#include "io/adm_writer.h"
|
||||
#include "foundation/fs_utf8.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
|
||||
#include "io/wav_writer.h" // pack_int24 (shared int24 quantisation)
|
||||
|
||||
namespace joc::io {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr long kDs64BodyOffset = 20;
|
||||
constexpr long kDataSizeOffset = 76;
|
||||
|
||||
void put_u16(std::string* out, std::uint16_t value) {
|
||||
char buffer[2];
|
||||
std::memcpy(buffer, &value, 2);
|
||||
out->append(buffer, 2);
|
||||
}
|
||||
|
||||
void put_u32(std::string* out, std::uint32_t value) {
|
||||
char buffer[4];
|
||||
std::memcpy(buffer, &value, 4);
|
||||
out->append(buffer, 4);
|
||||
}
|
||||
|
||||
void put_u64(std::string* out, std::uint64_t value) {
|
||||
char buffer[8];
|
||||
std::memcpy(buffer, &value, 8);
|
||||
out->append(buffer, 8);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AdmBwfWriter::~AdmBwfWriter() { abort(); }
|
||||
|
||||
Status AdmBwfWriter::open(const std::string& path, std::size_t block_samples) {
|
||||
if (block_samples < JOC_FRAME_SAMPLES) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOutput,
|
||||
"ADM block size must hold at least one E-AC-3 frame");
|
||||
}
|
||||
path_ = path;
|
||||
block_samples_ = block_samples;
|
||||
used_ = 0;
|
||||
frames_ = 0;
|
||||
finalized_ = false;
|
||||
buffer_.assign(block_samples * kChannels, 0.0f);
|
||||
|
||||
file_ = fs_utf8::fopen(path, "wb+");
|
||||
if (file_ == nullptr) {
|
||||
std::error_code ignored;
|
||||
const std::filesystem::path parent = std::filesystem::path(path).parent_path();
|
||||
if (!parent.empty()) {
|
||||
std::filesystem::create_directories(parent, ignored);
|
||||
}
|
||||
file_ = fs_utf8::fopen(path, "wb+");
|
||||
}
|
||||
if (file_ == nullptr) {
|
||||
return Status::fail(JOC_ERR_OUTPUT_OPEN, stage::kOutput, "cannot open " + path);
|
||||
}
|
||||
std::string header;
|
||||
header.append("RF64", 4);
|
||||
put_u32(&header, 0xFFFFFFFFu);
|
||||
header.append("WAVE", 4);
|
||||
if (std::fwrite(header.data(), 1, header.size(), file_) != header.size()) {
|
||||
abort();
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "cannot write " + path);
|
||||
}
|
||||
Status status = write_chunk("ds64", std::string(28, '\0'));
|
||||
if (!status.ok()) {
|
||||
abort();
|
||||
return status;
|
||||
}
|
||||
std::string fmt;
|
||||
put_u16(&fmt, 1);
|
||||
put_u16(&fmt, static_cast<std::uint16_t>(kChannels));
|
||||
put_u32(&fmt, kRate);
|
||||
put_u32(&fmt, kRate * kChannels * 3u);
|
||||
put_u16(&fmt, static_cast<std::uint16_t>(kChannels * 3u));
|
||||
put_u16(&fmt, 24);
|
||||
status = write_chunk("fmt ", fmt);
|
||||
if (!status.ok()) {
|
||||
abort();
|
||||
return status;
|
||||
}
|
||||
status = write_chunk("data", std::string());
|
||||
if (!status.ok()) {
|
||||
abort();
|
||||
return status;
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status AdmBwfWriter::write_chunk(const char id[4], const std::string& body) {
|
||||
std::string header;
|
||||
header.append(id, 4);
|
||||
put_u32(&header, static_cast<std::uint32_t>(body.size()));
|
||||
if (std::fwrite(header.data(), 1, header.size(), file_) != header.size()) {
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "chunk header write failed");
|
||||
}
|
||||
if (!body.empty() &&
|
||||
std::fwrite(body.data(), 1, body.size(), file_) != body.size()) {
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "chunk body write failed");
|
||||
}
|
||||
if ((body.size() & 1u) != 0u) {
|
||||
const char pad = '\0';
|
||||
if (std::fwrite(&pad, 1, 1, file_) != 1) {
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "chunk padding write failed");
|
||||
}
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status AdmBwfWriter::flush() {
|
||||
if (used_ == 0) {
|
||||
return Status::success();
|
||||
}
|
||||
if (file_ == nullptr) {
|
||||
return Status::fail(JOC_ERR_STATE, stage::kOutput, "ADM writer is not open");
|
||||
}
|
||||
packed_.clear();
|
||||
pack_int24(buffer_.data(), used_, kChannels, &packed_);
|
||||
if (std::fwrite(packed_.data(), 1, packed_.size(), file_) != packed_.size()) {
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "audio write failed for " + path_);
|
||||
}
|
||||
used_ = 0;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status AdmBwfWriter::write_objects16(const float* planar16) {
|
||||
if (file_ == nullptr) {
|
||||
return Status::fail(JOC_ERR_STATE, stage::kOutput, "ADM writer is not open");
|
||||
}
|
||||
if (planar16 == nullptr) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOutput, "null frame");
|
||||
}
|
||||
std::size_t source = 0;
|
||||
while (source < JOC_FRAME_SAMPLES) {
|
||||
const std::size_t available = block_samples_ - used_;
|
||||
const std::size_t count =
|
||||
std::min(available, static_cast<std::size_t>(JOC_FRAME_SAMPLES) - source);
|
||||
float* target = buffer_.data() + used_ * kChannels;
|
||||
std::memset(target, 0, count * kChannels * sizeof(float));
|
||||
for (std::size_t sample = 0; sample < count; ++sample) {
|
||||
float* row = target + sample * kChannels;
|
||||
row[3] = planar16[0u * JOC_FRAME_SAMPLES + source + sample];
|
||||
for (std::size_t object = 0; object < 15u; ++object) {
|
||||
row[10u + object] =
|
||||
planar16[(object + 1u) * JOC_FRAME_SAMPLES + source + sample];
|
||||
}
|
||||
}
|
||||
used_ += count;
|
||||
source += count;
|
||||
if (used_ == block_samples_) {
|
||||
const Status status = flush();
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
frames_ += JOC_FRAME_SAMPLES;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status AdmBwfWriter::finalize(const std::string& axml, const std::string& chna,
|
||||
const std::string& dbmd) {
|
||||
if (file_ == nullptr) {
|
||||
return Status::fail(JOC_ERR_STATE, stage::kOutput, "ADM writer is not open");
|
||||
}
|
||||
if (finalized_) {
|
||||
return Status::fail(JOC_ERR_STATE, stage::kOutput, "ADM writer already finalized");
|
||||
}
|
||||
Status status = flush();
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
status = write_chunk("axml", axml);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
status = write_chunk("chna", chna);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
status = write_chunk("dbmd", dbmd);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
if (std::fseek(file_, 0, SEEK_END) != 0) {
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "seek failed for " + path_);
|
||||
}
|
||||
const long long total = std::ftell(file_);
|
||||
if (total < 0) {
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "tell failed for " + path_);
|
||||
}
|
||||
const std::uint64_t data_len = frames_ * kChannels * 3u;
|
||||
const std::uint32_t data_field =
|
||||
data_len <= 0xFFFFFFFFull ? static_cast<std::uint32_t>(data_len) : 0xFFFFFFFFu;
|
||||
|
||||
if (std::fseek(file_, kDataSizeOffset, SEEK_SET) != 0) {
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "seek failed for " + path_);
|
||||
}
|
||||
char buffer[4];
|
||||
std::memcpy(buffer, &data_field, 4);
|
||||
if (std::fwrite(buffer, 1, 4, file_) != 4) {
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "data size patch failed");
|
||||
}
|
||||
|
||||
std::string ds64;
|
||||
put_u64(&ds64, static_cast<std::uint64_t>(total) - 8u);
|
||||
put_u64(&ds64, data_len);
|
||||
put_u64(&ds64, frames_);
|
||||
put_u32(&ds64, 0);
|
||||
if (std::fseek(file_, kDs64BodyOffset, SEEK_SET) != 0) {
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "seek failed for " + path_);
|
||||
}
|
||||
if (std::fwrite(ds64.data(), 1, ds64.size(), file_) != ds64.size()) {
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "ds64 patch failed");
|
||||
}
|
||||
finalized_ = true;
|
||||
if (std::fclose(file_) != 0) {
|
||||
file_ = nullptr;
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "close failed for " + path_);
|
||||
}
|
||||
file_ = nullptr;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
void AdmBwfWriter::abort() {
|
||||
if (file_ != nullptr) {
|
||||
std::fclose(file_);
|
||||
file_ = nullptr;
|
||||
}
|
||||
if (!finalized_ && !path_.empty()) {
|
||||
fs_utf8::remove(path_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace joc::io
|
||||
@@ -0,0 +1,52 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "foundation/status.h"
|
||||
#include "joc_core.h"
|
||||
|
||||
namespace joc::io {
|
||||
|
||||
class AdmBwfWriter {
|
||||
public:
|
||||
static constexpr std::uint32_t kChannels = 25;
|
||||
static constexpr std::uint32_t kRate = 48000;
|
||||
static constexpr std::size_t kDefaultBlockSamples = 131072;
|
||||
|
||||
AdmBwfWriter() = default;
|
||||
~AdmBwfWriter();
|
||||
|
||||
AdmBwfWriter(const AdmBwfWriter&) = delete;
|
||||
AdmBwfWriter& operator=(const AdmBwfWriter&) = delete;
|
||||
|
||||
Status open(const std::string& path, std::size_t block_samples = kDefaultBlockSamples);
|
||||
|
||||
Status write_objects16(const float* planar16);
|
||||
|
||||
Status finalize(const std::string& axml, const std::string& chna, const std::string& dbmd);
|
||||
|
||||
// Closes and removes a file that was never finalized (plan 28.3: abort must
|
||||
void abort();
|
||||
|
||||
std::uint64_t frames() const { return frames_; }
|
||||
bool open_ok() const { return file_ != nullptr; }
|
||||
|
||||
private:
|
||||
Status write_chunk(const char id[4], const std::string& body);
|
||||
Status flush();
|
||||
|
||||
std::FILE* file_ = nullptr;
|
||||
std::string path_;
|
||||
std::size_t block_samples_ = kDefaultBlockSamples;
|
||||
std::size_t used_ = 0;
|
||||
std::uint64_t frames_ = 0;
|
||||
std::vector<float> buffer_;
|
||||
std::string packed_;
|
||||
bool finalized_ = false;
|
||||
};
|
||||
|
||||
} // namespace joc::io
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "foundation/status.h"
|
||||
|
||||
// Read-only subset of the HDF5 file format, sized for the SOFA files this
|
||||
// project consumes: superblock v0, version 2 object headers, fractal-heap link
|
||||
// and attribute storage, compact and contiguous datasets. The file is opened
|
||||
// lazily: only the requested dataset's bytes are read into memory, everything
|
||||
// else (superblock, object headers, heap blocks) is fetched on demand and the
|
||||
// metadata that was parsed is cached by file address.
|
||||
//
|
||||
// Paths are HDF5 link paths ("Data.IR" is a single link name here, "Group/Set"
|
||||
// walks two links); the empty path names the root group. Byte order is
|
||||
// normalized on read, so callers never see the file's own endianness.
|
||||
namespace joc::io {
|
||||
|
||||
enum class Hdf5Type {
|
||||
Unknown,
|
||||
Int8,
|
||||
Int16,
|
||||
Int32,
|
||||
Int64,
|
||||
UInt8,
|
||||
UInt16,
|
||||
UInt32,
|
||||
UInt64,
|
||||
Float32,
|
||||
Float64,
|
||||
String,
|
||||
};
|
||||
|
||||
struct Hdf5TypeInfo {
|
||||
Hdf5Type type = Hdf5Type::Unknown;
|
||||
std::uint32_t size = 0; // bytes per element as stored in the file
|
||||
bool big_endian = false;
|
||||
bool is_signed = false;
|
||||
};
|
||||
|
||||
struct Hdf5DatasetInfo {
|
||||
std::vector<std::uint64_t> shape;
|
||||
Hdf5TypeInfo type;
|
||||
|
||||
std::uint64_t element_count() const;
|
||||
};
|
||||
|
||||
struct Hdf5Attribute {
|
||||
Hdf5TypeInfo type;
|
||||
std::vector<std::uint64_t> shape;
|
||||
std::vector<std::uint8_t> raw; // C order, host byte order
|
||||
std::string text; // decoded for fixed-length string attributes
|
||||
};
|
||||
|
||||
class Hdf5File {
|
||||
public:
|
||||
Hdf5File();
|
||||
~Hdf5File();
|
||||
Hdf5File(Hdf5File&&) noexcept;
|
||||
Hdf5File& operator=(Hdf5File&&) noexcept;
|
||||
Hdf5File(const Hdf5File&) = delete;
|
||||
Hdf5File& operator=(const Hdf5File&) = delete;
|
||||
|
||||
Status open(const std::string& path);
|
||||
bool is_open() const;
|
||||
|
||||
// Names of the links of a group ("" is the root group).
|
||||
Status links(const std::string& group_path, std::vector<std::string>* names) const;
|
||||
|
||||
bool has_dataset(const std::string& path) const;
|
||||
Status dataset_info(const std::string& path, Hdf5DatasetInfo* out) const;
|
||||
Status read_dataset_raw(const std::string& path, std::vector<std::uint8_t>* out) const;
|
||||
Status read_dataset_double(const std::string& path, std::vector<double>* out) const;
|
||||
|
||||
Status attribute_names(const std::string& object_path, std::vector<std::string>* names) const;
|
||||
Status attribute(const std::string& object_path, const std::string& name, Hdf5Attribute* out) const;
|
||||
Status attribute_text(const std::string& object_path, const std::string& name, std::string* out) const;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace joc::io
|
||||
@@ -0,0 +1,276 @@
|
||||
#include "io/inflate.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace joc::io {
|
||||
|
||||
namespace {
|
||||
|
||||
class LsbBitReader {
|
||||
public:
|
||||
LsbBitReader(const std::uint8_t* data, std::size_t size) : data_(data), size_(size) {}
|
||||
|
||||
bool ok() const { return ok_; }
|
||||
std::size_t byte_position() const { return position_ >> 3; }
|
||||
|
||||
std::uint32_t bits(unsigned count) {
|
||||
std::uint32_t value = 0;
|
||||
for (unsigned i = 0; i < count; ++i) {
|
||||
if ((position_ >> 3) >= size_) {
|
||||
ok_ = false;
|
||||
return value;
|
||||
}
|
||||
const std::uint32_t bit = (data_[position_ >> 3] >> (position_ & 7u)) & 1u;
|
||||
value |= bit << i;
|
||||
++position_;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void align_to_byte() { position_ = (position_ + 7u) & ~static_cast<std::size_t>(7u); }
|
||||
|
||||
void skip_bytes(std::size_t count) { position_ += count * 8u; }
|
||||
|
||||
private:
|
||||
const std::uint8_t* data_;
|
||||
std::size_t size_;
|
||||
std::size_t position_ = 0;
|
||||
bool ok_ = true;
|
||||
};
|
||||
|
||||
struct Huffman {
|
||||
std::uint16_t counts[16] = {};
|
||||
std::uint16_t symbols[288] = {};
|
||||
int max_length = 0;
|
||||
|
||||
bool build(const std::uint8_t* lengths, int count) {
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
counts[i] = 0;
|
||||
}
|
||||
for (int i = 0; i < count; ++i) {
|
||||
counts[lengths[i]]++;
|
||||
}
|
||||
counts[0] = 0;
|
||||
std::uint16_t offsets[16] = {};
|
||||
std::uint16_t total = 0;
|
||||
for (int length = 1; length < 16; ++length) {
|
||||
offsets[length] = total;
|
||||
total = static_cast<std::uint16_t>(total + counts[length]);
|
||||
}
|
||||
if (total == 0) {
|
||||
return false;
|
||||
}
|
||||
for (int symbol = 0; symbol < count; ++symbol) {
|
||||
const std::uint8_t length = lengths[symbol];
|
||||
if (length != 0) {
|
||||
symbols[offsets[length]++] = static_cast<std::uint16_t>(symbol);
|
||||
}
|
||||
}
|
||||
max_length = 15;
|
||||
while (max_length > 0 && counts[max_length] == 0) {
|
||||
--max_length;
|
||||
}
|
||||
return max_length != 0;
|
||||
}
|
||||
|
||||
int decode(LsbBitReader* reader) const {
|
||||
int code = 0;
|
||||
int first = 0;
|
||||
int index = 0;
|
||||
for (int length = 1; length <= max_length; ++length) {
|
||||
code |= static_cast<int>(reader->bits(1));
|
||||
if (!reader->ok()) {
|
||||
return -1;
|
||||
}
|
||||
const int count = counts[length];
|
||||
if (code - first < count) {
|
||||
return symbols[index + (code - first)];
|
||||
}
|
||||
index += count;
|
||||
first = (first + count) << 1;
|
||||
code <<= 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
constexpr std::uint16_t kLengthBase[29] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19,
|
||||
23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163,
|
||||
195, 227, 258};
|
||||
constexpr std::uint8_t kLengthExtra[29] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2,
|
||||
2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0};
|
||||
constexpr std::uint16_t kDistanceBase[30] = {1, 2, 3, 4, 5, 7, 9, 13,
|
||||
17, 25, 33, 49, 65, 97, 129, 193,
|
||||
257, 385, 513, 769, 1025, 1537, 2049, 3073,
|
||||
4097, 6145, 8193, 12289, 16385, 24577};
|
||||
constexpr std::uint8_t kDistanceExtra[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6,
|
||||
6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
|
||||
constexpr std::uint8_t kCodeLengthOrder[19] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2,
|
||||
14, 1, 15};
|
||||
|
||||
bool inflate_block_data(LsbBitReader* reader, const Huffman& literal, const Huffman& distance,
|
||||
std::vector<std::uint8_t>* out) {
|
||||
for (;;) {
|
||||
const int symbol = literal.decode(reader);
|
||||
if (symbol < 0) {
|
||||
return false;
|
||||
}
|
||||
if (symbol < 256) {
|
||||
out->push_back(static_cast<std::uint8_t>(symbol));
|
||||
continue;
|
||||
}
|
||||
if (symbol == 256) {
|
||||
return true;
|
||||
}
|
||||
const int length_index = symbol - 257;
|
||||
if (length_index >= 29) {
|
||||
return false;
|
||||
}
|
||||
const std::uint32_t length =
|
||||
kLengthBase[length_index] + reader->bits(kLengthExtra[length_index]);
|
||||
const int distance_symbol = distance.decode(reader);
|
||||
if (distance_symbol < 0 || distance_symbol >= 30) {
|
||||
return false;
|
||||
}
|
||||
const std::uint32_t distance_value =
|
||||
kDistanceBase[distance_symbol] + reader->bits(kDistanceExtra[distance_symbol]);
|
||||
if (!reader->ok() || distance_value == 0 || distance_value > out->size()) {
|
||||
return false;
|
||||
}
|
||||
const std::size_t start = out->size() - distance_value;
|
||||
for (std::uint32_t i = 0; i < length; ++i) {
|
||||
out->push_back((*out)[start + i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool inflate_fixed(LsbBitReader* reader, std::vector<std::uint8_t>* out) {
|
||||
std::uint8_t lengths[288];
|
||||
for (int i = 0; i < 144; ++i) { lengths[i] = 8; }
|
||||
for (int i = 144; i < 256; ++i) { lengths[i] = 9; }
|
||||
for (int i = 256; i < 280; ++i) { lengths[i] = 7; }
|
||||
for (int i = 280; i < 288; ++i) { lengths[i] = 8; }
|
||||
Huffman literal;
|
||||
if (!literal.build(lengths, 288)) {
|
||||
return false;
|
||||
}
|
||||
std::uint8_t distance_lengths[30];
|
||||
for (int i = 0; i < 30; ++i) { distance_lengths[i] = 5; }
|
||||
Huffman distance;
|
||||
if (!distance.build(distance_lengths, 30)) {
|
||||
return false;
|
||||
}
|
||||
return inflate_block_data(reader, literal, distance, out);
|
||||
}
|
||||
|
||||
bool inflate_dynamic(LsbBitReader* reader, std::vector<std::uint8_t>* out) {
|
||||
const int literal_count = static_cast<int>(reader->bits(5)) + 257;
|
||||
const int distance_count = static_cast<int>(reader->bits(5)) + 1;
|
||||
const int code_length_count = static_cast<int>(reader->bits(4)) + 4;
|
||||
if (!reader->ok() || literal_count > 286 || distance_count > 30) {
|
||||
return false;
|
||||
}
|
||||
std::uint8_t code_lengths[19] = {};
|
||||
for (int i = 0; i < code_length_count; ++i) {
|
||||
code_lengths[kCodeLengthOrder[i]] = static_cast<std::uint8_t>(reader->bits(3));
|
||||
}
|
||||
if (!reader->ok()) {
|
||||
return false;
|
||||
}
|
||||
Huffman code_length_tree;
|
||||
if (!code_length_tree.build(code_lengths, 19)) {
|
||||
return false;
|
||||
}
|
||||
std::uint8_t lengths[288 + 30] = {};
|
||||
const int total = literal_count + distance_count;
|
||||
int index = 0;
|
||||
while (index < total) {
|
||||
const int symbol = code_length_tree.decode(reader);
|
||||
if (symbol < 0) {
|
||||
return false;
|
||||
}
|
||||
if (symbol < 16) {
|
||||
lengths[index++] = static_cast<std::uint8_t>(symbol);
|
||||
continue;
|
||||
}
|
||||
int repeat = 0;
|
||||
std::uint8_t value = 0;
|
||||
if (symbol == 16) {
|
||||
if (index == 0) {
|
||||
return false;
|
||||
}
|
||||
value = lengths[index - 1];
|
||||
repeat = 3 + static_cast<int>(reader->bits(2));
|
||||
} else if (symbol == 17) {
|
||||
repeat = 3 + static_cast<int>(reader->bits(3));
|
||||
} else {
|
||||
repeat = 11 + static_cast<int>(reader->bits(7));
|
||||
}
|
||||
if (!reader->ok() || index + repeat > total) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < repeat; ++i) {
|
||||
lengths[index++] = value;
|
||||
}
|
||||
}
|
||||
Huffman literal;
|
||||
if (!literal.build(lengths, literal_count)) {
|
||||
return false;
|
||||
}
|
||||
Huffman distance;
|
||||
if (!distance.build(lengths + literal_count, distance_count)) {
|
||||
return false;
|
||||
}
|
||||
return inflate_block_data(reader, literal, distance, out);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool inflate_raw(const std::uint8_t* data, std::size_t size, std::vector<std::uint8_t>* out) {
|
||||
if (data == nullptr || out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
out->clear();
|
||||
LsbBitReader reader(data, size);
|
||||
for (;;) {
|
||||
const std::uint32_t final_block = reader.bits(1);
|
||||
const std::uint32_t type = reader.bits(2);
|
||||
if (!reader.ok()) {
|
||||
return false;
|
||||
}
|
||||
if (type == 0) {
|
||||
reader.align_to_byte();
|
||||
const std::size_t position = reader.byte_position();
|
||||
if (position + 4 > size) {
|
||||
return false;
|
||||
}
|
||||
const std::uint16_t length = static_cast<std::uint16_t>(data[position] | (data[position + 1] << 8));
|
||||
const std::uint16_t complement =
|
||||
static_cast<std::uint16_t>(data[position + 2] | (data[position + 3] << 8));
|
||||
if (static_cast<std::uint16_t>(length ^ 0xFFFFu) != complement) {
|
||||
return false;
|
||||
}
|
||||
if (position + 4 + length > size) {
|
||||
return false;
|
||||
}
|
||||
out->insert(out->end(), data + position + 4, data + position + 4 + length);
|
||||
reader.skip_bytes(4u + length);
|
||||
} else if (type == 1) {
|
||||
if (!inflate_fixed(&reader, out)) {
|
||||
return false;
|
||||
}
|
||||
} else if (type == 2) {
|
||||
if (!inflate_dynamic(&reader, out)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (final_block != 0u) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace joc::io
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace joc::io {
|
||||
|
||||
bool inflate_raw(const std::uint8_t* data, std::size_t size, std::vector<std::uint8_t>* out);
|
||||
|
||||
} // namespace joc::io
|
||||
@@ -0,0 +1,420 @@
|
||||
#include "io/npy.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace joc::io {
|
||||
|
||||
namespace {
|
||||
|
||||
std::uint16_t read_u16(const std::uint8_t* p) { return static_cast<std::uint16_t>(p[0] | (p[1] << 8)); }
|
||||
std::uint32_t read_u32(const std::uint8_t* p) {
|
||||
return static_cast<std::uint32_t>(p[0]) | (static_cast<std::uint32_t>(p[1]) << 8) |
|
||||
(static_cast<std::uint32_t>(p[2]) << 16) | (static_cast<std::uint32_t>(p[3]) << 24);
|
||||
}
|
||||
|
||||
NpyType classify(const std::string& descr) {
|
||||
if (descr == "<f8" || descr == "=f8" || descr == "|f8") { return NpyType::Float64; }
|
||||
if (descr == "<f4" || descr == "=f4") { return NpyType::Float32; }
|
||||
if (descr == "<i8" || descr == "=i8") { return NpyType::Int64; }
|
||||
if (descr == "<i4" || descr == "=i4") { return NpyType::Int32; }
|
||||
if (descr == "<i2" || descr == "=i2") { return NpyType::Int16; }
|
||||
if (descr == "|u1" || descr == "<u1") { return NpyType::UInt8; }
|
||||
if (descr == "<c16" || descr == "=c16") { return NpyType::Complex128; }
|
||||
if (descr.size() > 2 && descr[0] == '<' && descr[1] == 'U') {
|
||||
return NpyType::Unicode;
|
||||
}
|
||||
if (descr.size() > 2 && descr[0] == '=' && descr[1] == 'U') {
|
||||
return NpyType::Unicode;
|
||||
}
|
||||
return NpyType::Unknown;
|
||||
}
|
||||
|
||||
std::size_t unicode_length(const std::string& descr) {
|
||||
std::size_t index = 0;
|
||||
while (index < descr.size() && (descr[index] == '<' || descr[index] == '=')) {
|
||||
++index;
|
||||
}
|
||||
if (index >= descr.size() || descr[index] != 'U') {
|
||||
return 0;
|
||||
}
|
||||
++index;
|
||||
std::size_t value = 0;
|
||||
bool any = false;
|
||||
while (index < descr.size() && descr[index] >= '0' && descr[index] <= '9') {
|
||||
value = value * 10 + static_cast<std::size_t>(descr[index] - '0');
|
||||
++index;
|
||||
any = true;
|
||||
}
|
||||
return any ? value : 0;
|
||||
}
|
||||
|
||||
bool is_big_endian(const std::string& descr) { return !descr.empty() && descr[0] == '>'; }
|
||||
|
||||
bool header_value(const std::string& header, const std::string& key, std::string* out) {
|
||||
const std::string needle = "'" + key + "'";
|
||||
const std::size_t position = header.find(needle);
|
||||
if (position == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
const std::size_t colon = header.find(':', position + needle.size());
|
||||
if (colon == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
std::size_t start = colon + 1;
|
||||
while (start < header.size() && (header[start] == ' ' || header[start] == '\t')) {
|
||||
++start;
|
||||
}
|
||||
*out = header.substr(start);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::size_t NpyArray::element_count() const {
|
||||
std::size_t count = 1;
|
||||
for (const std::int64_t dimension : shape) {
|
||||
count *= static_cast<std::size_t>(dimension < 0 ? 0 : dimension);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
std::size_t NpyArray::element_size() const {
|
||||
switch (type) {
|
||||
case NpyType::Float64: return 8;
|
||||
case NpyType::Float32: return 4;
|
||||
case NpyType::Int64: return 8;
|
||||
case NpyType::Int32: return 4;
|
||||
case NpyType::Int16: return 2;
|
||||
case NpyType::UInt8: return 1;
|
||||
case NpyType::Complex128: return 16;
|
||||
case NpyType::Unicode: return item_bytes;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool parse_npy(const std::uint8_t* data, std::size_t size, NpyArray* out, std::string* error) {
|
||||
if (data == nullptr || out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const std::uint8_t magic[6] = {0x93u, 'N', 'U', 'M', 'P', 'Y'};
|
||||
if (size < 10u || std::memcmp(data, magic, 6) != 0) {
|
||||
if (error != nullptr) { *error = "not a .npy image"; }
|
||||
return false;
|
||||
}
|
||||
const std::uint8_t major = data[6];
|
||||
std::size_t header_length = 0;
|
||||
std::size_t header_offset = 0;
|
||||
if (major == 1u) {
|
||||
header_length = read_u16(data + 8);
|
||||
header_offset = 10;
|
||||
} else if (major == 2u || major == 3u) {
|
||||
if (size < 12u) {
|
||||
if (error != nullptr) { *error = "truncated .npy v2 header"; }
|
||||
return false;
|
||||
}
|
||||
header_length = read_u32(data + 8);
|
||||
header_offset = 12;
|
||||
} else {
|
||||
if (error != nullptr) { *error = "unsupported .npy version " + std::to_string(major); }
|
||||
return false;
|
||||
}
|
||||
if (header_offset + header_length > size) {
|
||||
if (error != nullptr) { *error = "truncated .npy header"; }
|
||||
return false;
|
||||
}
|
||||
const std::string header(reinterpret_cast<const char*>(data + header_offset), header_length);
|
||||
|
||||
out->descr.clear();
|
||||
std::string value;
|
||||
if (!header_value(header, "descr", &value)) {
|
||||
if (error != nullptr) { *error = ".npy header without descr"; }
|
||||
return false;
|
||||
}
|
||||
const std::size_t first_quote = value.find('\'');
|
||||
const std::size_t second_quote =
|
||||
first_quote == std::string::npos ? std::string::npos : value.find('\'', first_quote + 1);
|
||||
if (first_quote == std::string::npos || second_quote == std::string::npos) {
|
||||
if (error != nullptr) { *error = ".npy descr is not a quoted string"; }
|
||||
return false;
|
||||
}
|
||||
out->descr = value.substr(first_quote + 1, second_quote - first_quote - 1);
|
||||
out->type = classify(out->descr);
|
||||
if (out->type == NpyType::Unknown) {
|
||||
if (error != nullptr) { *error = "unsupported .npy dtype " + out->descr; }
|
||||
return false;
|
||||
}
|
||||
out->item_bytes = 0;
|
||||
if (out->type == NpyType::Unicode) {
|
||||
const std::size_t length = unicode_length(out->descr);
|
||||
if (length == 0) {
|
||||
if (error != nullptr) { *error = "malformed unicode .npy dtype " + out->descr; }
|
||||
return false;
|
||||
}
|
||||
out->item_bytes = length * 4u;
|
||||
}
|
||||
|
||||
out->fortran_order = header.find("'fortran_order': True") != std::string::npos;
|
||||
|
||||
if (!header_value(header, "shape", &value)) {
|
||||
if (error != nullptr) { *error = ".npy header without shape"; }
|
||||
return false;
|
||||
}
|
||||
out->shape.clear();
|
||||
for (std::size_t i = 0; i < value.size(); ++i) {
|
||||
if (value[i] >= '0' && value[i] <= '9') {
|
||||
long long dimension = 0;
|
||||
while (i < value.size() && value[i] >= '0' && value[i] <= '9') {
|
||||
dimension = dimension * 10 + (value[i] - '0');
|
||||
++i;
|
||||
}
|
||||
out->shape.push_back(dimension);
|
||||
} else if (value[i] == ')') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const std::size_t expected = out->element_count() * out->element_size();
|
||||
if (header_offset + header_length + expected > size) {
|
||||
if (error != nullptr) {
|
||||
*error = ".npy payload truncated (need " + std::to_string(expected) + " bytes)";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
out->data = data + header_offset + header_length;
|
||||
out->data_bytes = expected;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool npy_shape_is(const NpyArray& array, const std::vector<std::int64_t>& expected) {
|
||||
return array.shape == expected;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename T>
|
||||
void load_le(const std::uint8_t* source, std::size_t count, bool swap, std::vector<T>* out) {
|
||||
out->resize(count);
|
||||
std::memcpy(out->data(), source, count * sizeof(T));
|
||||
if (swap) {
|
||||
std::uint8_t* bytes = reinterpret_cast<std::uint8_t*>(out->data());
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
for (std::size_t b = 0; b < sizeof(T) / 2; ++b) {
|
||||
const std::uint8_t temporary = bytes[i * sizeof(T) + b];
|
||||
bytes[i * sizeof(T) + b] = bytes[i * sizeof(T) + sizeof(T) - 1 - b];
|
||||
bytes[i * sizeof(T) + sizeof(T) - 1 - b] = temporary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool npy_to_double(const NpyArray& array, std::vector<double>* out, std::string* error) {
|
||||
const bool swap = is_big_endian(array.descr);
|
||||
const std::size_t count = array.element_count();
|
||||
switch (array.type) {
|
||||
case NpyType::Float64:
|
||||
load_le(array.data, count, swap, out);
|
||||
return true;
|
||||
case NpyType::Complex128:
|
||||
load_le(array.data, count * 2u, swap, out);
|
||||
return true;
|
||||
case NpyType::Float32: {
|
||||
std::vector<float> values;
|
||||
load_le(array.data, count, swap, &values);
|
||||
out->resize(count);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
(*out)[i] = static_cast<double>(values[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case NpyType::Int64: {
|
||||
std::vector<std::int64_t> values;
|
||||
load_le(array.data, count, swap, &values);
|
||||
out->resize(count);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
(*out)[i] = static_cast<double>(values[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case NpyType::Int32: {
|
||||
std::vector<std::int32_t> values;
|
||||
load_le(array.data, count, swap, &values);
|
||||
out->resize(count);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
(*out)[i] = static_cast<double>(values[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case NpyType::Int16: {
|
||||
std::vector<std::int16_t> values;
|
||||
load_le(array.data, count, swap, &values);
|
||||
out->resize(count);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
(*out)[i] = static_cast<double>(values[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case NpyType::UInt8: {
|
||||
out->resize(count);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
(*out)[i] = static_cast<double>(array.data[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
if (error != nullptr) { *error = "cannot convert " + array.descr + " to double"; }
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool npy_to_int16(const NpyArray& array, std::vector<std::int16_t>* out, std::string* error) {
|
||||
const bool swap = is_big_endian(array.descr);
|
||||
const std::size_t count = array.element_count();
|
||||
switch (array.type) {
|
||||
case NpyType::Int16:
|
||||
load_le(array.data, count, swap, out);
|
||||
return true;
|
||||
case NpyType::Int32: {
|
||||
std::vector<std::int32_t> values;
|
||||
load_le(array.data, count, swap, &values);
|
||||
out->resize(count);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
(*out)[i] = static_cast<std::int16_t>(values[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case NpyType::Int64: {
|
||||
std::vector<std::int64_t> values;
|
||||
load_le(array.data, count, swap, &values);
|
||||
out->resize(count);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
(*out)[i] = static_cast<std::int16_t>(values[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
if (error != nullptr) { *error = "cannot convert " + array.descr + " to int16"; }
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool npy_to_int32(const NpyArray& array, std::vector<std::int32_t>* out, std::string* error) {
|
||||
const bool swap = is_big_endian(array.descr);
|
||||
const std::size_t count = array.element_count();
|
||||
switch (array.type) {
|
||||
case NpyType::Int32:
|
||||
load_le(array.data, count, swap, out);
|
||||
return true;
|
||||
case NpyType::Int64: {
|
||||
std::vector<std::int64_t> values;
|
||||
load_le(array.data, count, swap, &values);
|
||||
out->resize(count);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
(*out)[i] = static_cast<std::int32_t>(values[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case NpyType::Int16: {
|
||||
std::vector<std::int16_t> values;
|
||||
load_le(array.data, count, swap, &values);
|
||||
out->resize(count);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
(*out)[i] = static_cast<std::int32_t>(values[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
if (error != nullptr) { *error = "cannot convert " + array.descr + " to int32"; }
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool npy_to_uint8(const NpyArray& array, std::vector<std::uint8_t>* out, std::string* error) {
|
||||
if (array.type != NpyType::UInt8) {
|
||||
if (error != nullptr) { *error = "cannot convert " + array.descr + " to uint8"; }
|
||||
return false;
|
||||
}
|
||||
out->assign(array.data, array.data + array.element_count());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool npy_unicode_to_utf8(const NpyArray& array, std::string* out, std::string* error) {
|
||||
if (array.type != NpyType::Unicode) {
|
||||
if (error != nullptr) { *error = "not a unicode .npy member: " + array.descr; }
|
||||
return false;
|
||||
}
|
||||
if (array.shape.size() != 0) {
|
||||
if (error != nullptr) { *error = "unicode .npy member must be a scalar"; }
|
||||
return false;
|
||||
}
|
||||
out->clear();
|
||||
const std::size_t count = array.item_bytes / 4u;
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
const std::uint8_t* p = array.data + i * 4u;
|
||||
const std::uint32_t code = static_cast<std::uint32_t>(p[0]) | (static_cast<std::uint32_t>(p[1]) << 8) |
|
||||
(static_cast<std::uint32_t>(p[2]) << 16) |
|
||||
(static_cast<std::uint32_t>(p[3]) << 24);
|
||||
if (code == 0u) {
|
||||
break;
|
||||
}
|
||||
if (code < 0x80u) {
|
||||
out->push_back(static_cast<char>(code));
|
||||
} else if (code < 0x800u) {
|
||||
out->push_back(static_cast<char>(0xC0u | (code >> 6)));
|
||||
out->push_back(static_cast<char>(0x80u | (code & 0x3Fu)));
|
||||
} else if (code < 0x10000u) {
|
||||
out->push_back(static_cast<char>(0xE0u | (code >> 12)));
|
||||
out->push_back(static_cast<char>(0x80u | ((code >> 6) & 0x3Fu)));
|
||||
out->push_back(static_cast<char>(0x80u | (code & 0x3Fu)));
|
||||
} else {
|
||||
out->push_back(static_cast<char>(0xF0u | (code >> 18)));
|
||||
out->push_back(static_cast<char>(0x80u | ((code >> 12) & 0x3Fu)));
|
||||
out->push_back(static_cast<char>(0x80u | ((code >> 6) & 0x3Fu)));
|
||||
out->push_back(static_cast<char>(0x80u | (code & 0x3Fu)));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool npy_to_c_order(const NpyArray& array, std::vector<std::uint8_t>* out, std::string* error) {
|
||||
const std::size_t element = array.element_size();
|
||||
if (element == 0) {
|
||||
if (error != nullptr) { *error = "unsupported element size for " + array.descr; }
|
||||
return false;
|
||||
}
|
||||
if (!array.fortran_order) {
|
||||
out->assign(array.data, array.data + array.data_bytes);
|
||||
return true;
|
||||
}
|
||||
const std::size_t dimensions = array.shape.size();
|
||||
if (dimensions == 0) {
|
||||
out->assign(array.data, array.data + element);
|
||||
return true;
|
||||
}
|
||||
// Source (Fortran) strides in elements; destination is C order.
|
||||
std::vector<std::size_t> source_stride(dimensions, 1);
|
||||
std::size_t running = 1;
|
||||
for (std::size_t d = 0; d < dimensions; ++d) {
|
||||
source_stride[d] = running;
|
||||
running *= static_cast<std::size_t>(array.shape[d]);
|
||||
}
|
||||
out->assign(array.data_bytes, 0);
|
||||
std::vector<std::size_t> index(dimensions, 0);
|
||||
const std::size_t total = array.element_count();
|
||||
for (std::size_t linear = 0; linear < total; ++linear) {
|
||||
std::size_t remainder = linear;
|
||||
for (std::size_t d = dimensions; d-- > 0;) {
|
||||
index[d] = remainder % static_cast<std::size_t>(array.shape[d]);
|
||||
remainder /= static_cast<std::size_t>(array.shape[d]);
|
||||
}
|
||||
std::size_t source = 0;
|
||||
for (std::size_t d = 0; d < dimensions; ++d) {
|
||||
source += index[d] * source_stride[d];
|
||||
}
|
||||
std::memcpy(out->data() + linear * element, array.data + source * element, element);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace joc::io
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace joc::io {
|
||||
|
||||
enum class NpyType { Unknown, Float64, Float32, Int64, Int32, Int16, UInt8, Complex128, Unicode };
|
||||
|
||||
struct NpyArray {
|
||||
std::string descr;
|
||||
NpyType type = NpyType::Unknown;
|
||||
bool fortran_order = false;
|
||||
std::vector<std::int64_t> shape;
|
||||
const std::uint8_t* data = nullptr;
|
||||
std::size_t data_bytes = 0;
|
||||
std::size_t item_bytes = 0; // bytes per element as stored
|
||||
|
||||
std::size_t element_count() const;
|
||||
std::size_t element_size() const; // bytes per element in the file
|
||||
};
|
||||
|
||||
// Parses the header of one `.npy` image. `data` must outlive the NpyArray.
|
||||
bool parse_npy(const std::uint8_t* data, std::size_t size, NpyArray* out, std::string* error);
|
||||
|
||||
bool npy_to_double(const NpyArray& array, std::vector<double>* out, std::string* error);
|
||||
bool npy_to_int16(const NpyArray& array, std::vector<std::int16_t>* out, std::string* error);
|
||||
bool npy_to_int32(const NpyArray& array, std::vector<std::int32_t>* out, std::string* error);
|
||||
bool npy_to_uint8(const NpyArray& array, std::vector<std::uint8_t>* out, std::string* error);
|
||||
|
||||
bool npy_unicode_to_utf8(const NpyArray& array, std::string* out, std::string* error);
|
||||
|
||||
// Materializes the array in C order as raw element bytes. Fortran-order members
|
||||
// hybrid synthesis table as [count][4] row-major while the shipped table stores it
|
||||
// Fortran-order, so passing the file bytes straight through would transpose it.
|
||||
bool npy_to_c_order(const NpyArray& array, std::vector<std::uint8_t>* out, std::string* error);
|
||||
|
||||
bool npy_shape_is(const NpyArray& array, const std::vector<std::int64_t>& expected);
|
||||
|
||||
} // namespace joc::io
|
||||
@@ -0,0 +1,263 @@
|
||||
#include "io/npy_writer.h"
|
||||
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "foundation/fs_utf8.h"
|
||||
#include "io/zip_reader.h"
|
||||
|
||||
namespace joc::io {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kNpyHeaderAlignment = 64;
|
||||
|
||||
void append_u16(std::vector<std::uint8_t>* out, std::uint16_t value) {
|
||||
out->push_back(static_cast<std::uint8_t>(value & 0xFFu));
|
||||
out->push_back(static_cast<std::uint8_t>((value >> 8) & 0xFFu));
|
||||
}
|
||||
|
||||
void append_u32(std::vector<std::uint8_t>* out, std::uint32_t value) {
|
||||
for (int index = 0; index < 4; ++index) {
|
||||
out->push_back(static_cast<std::uint8_t>((value >> (8 * index)) & 0xFFu));
|
||||
}
|
||||
}
|
||||
|
||||
void append_bytes(std::vector<std::uint8_t>* out, const void* data, std::size_t size) {
|
||||
const std::uint8_t* bytes = static_cast<const std::uint8_t*>(data);
|
||||
out->insert(out->end(), bytes, bytes + size);
|
||||
}
|
||||
|
||||
std::string shape_literal(const std::vector<std::uint64_t>& shape) {
|
||||
if (shape.empty()) {
|
||||
return "()";
|
||||
}
|
||||
std::string text = "(";
|
||||
for (std::size_t index = 0; index < shape.size(); ++index) {
|
||||
if (index != 0u) {
|
||||
text += ", ";
|
||||
}
|
||||
text += std::to_string(shape[index]);
|
||||
}
|
||||
if (shape.size() == 1u) {
|
||||
text += ",";
|
||||
}
|
||||
text += ")";
|
||||
return text;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<std::uint8_t> npy_image(const std::string& descr,
|
||||
const std::vector<std::uint64_t>& shape,
|
||||
const std::vector<std::uint8_t>& data) {
|
||||
std::string header = "{'descr': '" + descr + "', 'fortran_order': False, 'shape': " +
|
||||
shape_literal(shape) + ", }";
|
||||
// NumPy pads the header so that the payload starts on a 64-byte boundary.
|
||||
const std::size_t preamble = 10u; // magic, version, two byte header length
|
||||
std::size_t total = preamble + header.size() + 1u;
|
||||
const std::size_t padding = (kNpyHeaderAlignment - (total % kNpyHeaderAlignment)) %
|
||||
kNpyHeaderAlignment;
|
||||
header.append(padding, ' ');
|
||||
header.push_back('\n');
|
||||
|
||||
std::vector<std::uint8_t> out;
|
||||
out.reserve(preamble + header.size() + data.size());
|
||||
static const std::uint8_t kMagic[6] = {0x93u, 'N', 'U', 'M', 'P', 'Y'};
|
||||
append_bytes(&out, kMagic, sizeof(kMagic));
|
||||
out.push_back(1u); // major
|
||||
out.push_back(0u); // minor
|
||||
append_u16(&out, static_cast<std::uint16_t>(header.size()));
|
||||
append_bytes(&out, header.data(), header.size());
|
||||
append_bytes(&out, data.data(), data.size());
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> zip_bytes(const std::vector<NpyMember>& members) {
|
||||
std::vector<std::uint8_t> out;
|
||||
struct Entry {
|
||||
std::string name;
|
||||
std::uint32_t crc = 0;
|
||||
std::uint32_t size = 0;
|
||||
std::uint32_t offset = 0;
|
||||
};
|
||||
std::vector<Entry> entries;
|
||||
entries.reserve(members.size());
|
||||
|
||||
for (const NpyMember& member : members) {
|
||||
const std::string name = member.name + ".npy";
|
||||
const std::vector<std::uint8_t> payload = npy_image(member.descr, member.shape, member.data);
|
||||
Entry entry;
|
||||
entry.name = name;
|
||||
entry.crc = crc32_of(payload.data(), payload.size());
|
||||
entry.size = static_cast<std::uint32_t>(payload.size());
|
||||
entry.offset = static_cast<std::uint32_t>(out.size());
|
||||
entries.push_back(entry);
|
||||
|
||||
append_u32(&out, 0x04034B50u); // local file header
|
||||
append_u16(&out, 20u); // version needed
|
||||
append_u16(&out, 0u); // flags
|
||||
append_u16(&out, 0u); // method: stored
|
||||
append_u16(&out, 0u); // time
|
||||
append_u16(&out, 0x2821u); // date: 2000-01-01, fixed for reproducibility
|
||||
append_u32(&out, entry.crc);
|
||||
append_u32(&out, entry.size);
|
||||
append_u32(&out, entry.size);
|
||||
append_u16(&out, static_cast<std::uint16_t>(name.size()));
|
||||
append_u16(&out, 0u); // extra length
|
||||
append_bytes(&out, name.data(), name.size());
|
||||
append_bytes(&out, payload.data(), payload.size());
|
||||
}
|
||||
|
||||
const std::uint32_t directory_offset = static_cast<std::uint32_t>(out.size());
|
||||
for (const Entry& entry : entries) {
|
||||
append_u32(&out, 0x02014B50u); // central directory header
|
||||
append_u16(&out, 20u); // version made by
|
||||
append_u16(&out, 20u); // version needed
|
||||
append_u16(&out, 0u); // flags
|
||||
append_u16(&out, 0u); // method: stored
|
||||
append_u16(&out, 0u); // time
|
||||
append_u16(&out, 0x2821u); // date
|
||||
append_u32(&out, entry.crc);
|
||||
append_u32(&out, entry.size);
|
||||
append_u32(&out, entry.size);
|
||||
append_u16(&out, static_cast<std::uint16_t>(entry.name.size()));
|
||||
append_u16(&out, 0u); // extra
|
||||
append_u16(&out, 0u); // comment
|
||||
append_u16(&out, 0u); // disk
|
||||
append_u16(&out, 0u); // internal attributes
|
||||
append_u32(&out, 0u); // external attributes
|
||||
append_u32(&out, entry.offset);
|
||||
append_bytes(&out, entry.name.data(), entry.name.size());
|
||||
}
|
||||
const std::uint32_t directory_size = static_cast<std::uint32_t>(out.size()) - directory_offset;
|
||||
|
||||
append_u32(&out, 0x06054B50u); // end of central directory
|
||||
append_u16(&out, 0u);
|
||||
append_u16(&out, 0u);
|
||||
append_u16(&out, static_cast<std::uint16_t>(entries.size()));
|
||||
append_u16(&out, static_cast<std::uint16_t>(entries.size()));
|
||||
append_u32(&out, directory_size);
|
||||
append_u32(&out, directory_offset);
|
||||
append_u16(&out, 0u);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool write_zip(const std::string& path, const std::vector<NpyMember>& members,
|
||||
std::string* error) {
|
||||
const std::vector<std::uint8_t> bytes = zip_bytes(members);
|
||||
std::FILE* stream = fs_utf8::fopen(path, "wb");
|
||||
if (stream == nullptr) {
|
||||
if (error != nullptr) {
|
||||
*error = "cannot open " + path + " for writing";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const std::size_t written = std::fwrite(bytes.data(), 1, bytes.size(), stream);
|
||||
const bool flushed = std::fclose(stream) == 0;
|
||||
if (written != bytes.size() || !flushed) {
|
||||
if (error != nullptr) {
|
||||
*error = "short write to " + path;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> utf8_to_utf32le(const std::string& text) {
|
||||
std::vector<std::uint8_t> out;
|
||||
out.reserve(text.size() * 4u);
|
||||
std::size_t index = 0;
|
||||
while (index < text.size()) {
|
||||
const std::uint8_t lead = static_cast<std::uint8_t>(text[index]);
|
||||
std::uint32_t code = 0;
|
||||
std::size_t extra = 0;
|
||||
if (lead < 0x80u) {
|
||||
code = lead;
|
||||
} else if ((lead & 0xE0u) == 0xC0u) {
|
||||
code = lead & 0x1Fu;
|
||||
extra = 1;
|
||||
} else if ((lead & 0xF0u) == 0xE0u) {
|
||||
code = lead & 0x0Fu;
|
||||
extra = 2;
|
||||
} else if ((lead & 0xF8u) == 0xF0u) {
|
||||
code = lead & 0x07u;
|
||||
extra = 3;
|
||||
} else {
|
||||
code = 0xFFFDu; // invalid lead byte: substitute rather than fail
|
||||
extra = 0;
|
||||
}
|
||||
++index;
|
||||
for (std::size_t count = 0; count < extra && index < text.size(); ++count) {
|
||||
code = (code << 6) | (static_cast<std::uint8_t>(text[index]) & 0x3Fu);
|
||||
++index;
|
||||
}
|
||||
for (int byte = 0; byte < 4; ++byte) {
|
||||
out.push_back(static_cast<std::uint8_t>((code >> (8 * byte)) & 0xFFu));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string python_float_repr(double value) {
|
||||
if (std::isnan(value)) {
|
||||
return "NaN";
|
||||
}
|
||||
if (std::isinf(value)) {
|
||||
return value > 0.0 ? "Infinity" : "-Infinity";
|
||||
}
|
||||
// to_chars gives the shortest round-trip digits; Python's repr uses the same
|
||||
// digits but its own notation, so the digits are re-laid-out here.
|
||||
std::array<char, 64> buffer{};
|
||||
const std::to_chars_result converted =
|
||||
std::to_chars(buffer.data(), buffer.data() + buffer.size(), value);
|
||||
std::string text(buffer.data(), converted.ptr);
|
||||
const bool negative = !text.empty() && text[0] == '-';
|
||||
const std::string body = negative ? text.substr(1) : text;
|
||||
const std::size_t exponent_at = body.find_first_of("eE");
|
||||
std::string digits = body;
|
||||
int exponent = 0;
|
||||
if (exponent_at != std::string::npos) {
|
||||
digits = body.substr(0, exponent_at);
|
||||
exponent = std::atoi(body.c_str() + exponent_at + 1);
|
||||
}
|
||||
const std::size_t point = digits.find('.');
|
||||
std::string mantissa = digits;
|
||||
if (point != std::string::npos) {
|
||||
mantissa = digits.substr(0, point) + digits.substr(point + 1);
|
||||
exponent += static_cast<int>(point) - 1;
|
||||
} else {
|
||||
exponent += static_cast<int>(digits.size()) - 1;
|
||||
}
|
||||
while (mantissa.size() > 1u && mantissa.back() == '0') {
|
||||
mantissa.pop_back();
|
||||
}
|
||||
// Python switches to exponent notation below 1e-4 and at 1e16 and above.
|
||||
std::string result;
|
||||
if (exponent < -4 || exponent >= 16) {
|
||||
result = mantissa.substr(0, 1);
|
||||
if (mantissa.size() > 1u) {
|
||||
result += "." + mantissa.substr(1);
|
||||
}
|
||||
char tail[16];
|
||||
std::snprintf(tail, sizeof(tail), "e%+03d", exponent);
|
||||
result += tail;
|
||||
} else if (exponent >= 0) {
|
||||
if (static_cast<std::size_t>(exponent) + 1u >= mantissa.size()) {
|
||||
result = mantissa + std::string(static_cast<std::size_t>(exponent) + 1u - mantissa.size(), '0');
|
||||
result += ".0";
|
||||
} else {
|
||||
result = mantissa.substr(0, static_cast<std::size_t>(exponent) + 1u) + "." +
|
||||
mantissa.substr(static_cast<std::size_t>(exponent) + 1u);
|
||||
}
|
||||
} else {
|
||||
result = "0." + std::string(static_cast<std::size_t>(-exponent - 1), '0') + mantissa;
|
||||
}
|
||||
return negative ? "-" + result : result;
|
||||
}
|
||||
|
||||
} // namespace joc::io
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace joc::io {
|
||||
|
||||
// NPY 1.0 images and a minimal ZIP container, used to write the compiled HRTF
|
||||
// cache in exactly the layout the reader (and NumPy) expects. Only what the
|
||||
// cache needs is implemented: little-endian C-order arrays and stored members.
|
||||
struct NpyMember {
|
||||
std::string name; // archive member name, without the .npy suffix
|
||||
std::string descr; // NumPy dtype string, e.g. "<f8", "<c16", "<U123"
|
||||
std::vector<std::uint64_t> shape;
|
||||
std::vector<std::uint8_t> data; // C order payload in the dtype's byte order
|
||||
};
|
||||
|
||||
// Serializes one array as an NPY 1.0 image (magic, header, 64-byte aligned).
|
||||
std::vector<std::uint8_t> npy_image(const std::string& descr,
|
||||
const std::vector<std::uint64_t>& shape,
|
||||
const std::vector<std::uint8_t>& data);
|
||||
|
||||
// Writes a ZIP archive with stored (uncompressed) members. The upstream reader
|
||||
// accepts stored members, and compression would need a deflate encoder.
|
||||
bool write_zip(const std::string& path, const std::vector<NpyMember>& members,
|
||||
std::string* error);
|
||||
|
||||
// Serializes the archive in memory (same layout as write_zip).
|
||||
std::vector<std::uint8_t> zip_bytes(const std::vector<NpyMember>& members);
|
||||
|
||||
// UTF-8 text as the payload of a NumPy Unicode scalar string ('<U<n>').
|
||||
std::vector<std::uint8_t> utf8_to_utf32le(const std::string& text);
|
||||
|
||||
// Python's repr() for a double: shortest round-trip digits with Python's
|
||||
// exponent rules, which is what json.dumps emits for the cache metadata.
|
||||
std::string python_float_repr(double value);
|
||||
|
||||
} // namespace joc::io
|
||||
@@ -0,0 +1,186 @@
|
||||
#include "io/process.h"
|
||||
#include "foundation/fs_utf8.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/wait.h>
|
||||
#endif
|
||||
|
||||
namespace joc::io {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string quote_argument(const std::string& argument) {
|
||||
if (!argument.empty() && argument.find_first_of(" \t\"") == std::string::npos) {
|
||||
return argument;
|
||||
}
|
||||
std::string quoted = "\"";
|
||||
unsigned backslashes = 0;
|
||||
for (const char c : argument) {
|
||||
if (c == '\\') {
|
||||
++backslashes;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') {
|
||||
quoted.append(backslashes * 2 + 1, '\\');
|
||||
quoted.push_back('"');
|
||||
backslashes = 0;
|
||||
continue;
|
||||
}
|
||||
quoted.append(backslashes, '\\');
|
||||
backslashes = 0;
|
||||
quoted.push_back(c);
|
||||
}
|
||||
quoted.append(backslashes * 2, '\\');
|
||||
quoted.push_back('"');
|
||||
return quoted;
|
||||
}
|
||||
|
||||
std::string tail_of(const std::string& text, std::size_t limit) {
|
||||
if (text.size() <= limit) {
|
||||
return text;
|
||||
}
|
||||
return text.substr(text.size() - limit);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status run_process(const std::vector<std::string>& argv, ProcessResult* out) {
|
||||
if (out == nullptr || argv.empty()) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOutput, "empty command");
|
||||
}
|
||||
out->output.clear();
|
||||
out->exit_code = 0;
|
||||
|
||||
const std::filesystem::path log_path =
|
||||
std::filesystem::temp_directory_path() /
|
||||
("joc_process_" + std::to_string(std::random_device{}()) + ".log");
|
||||
|
||||
auto read_log = [&]() {
|
||||
#if defined(_WIN32)
|
||||
return; // the Windows branch reads the handle it opened
|
||||
#else
|
||||
std::ifstream log = fs_utf8::open_input(fs_utf8::from_path(log_path));
|
||||
if (log) {
|
||||
std::string text((std::istreambuf_iterator<char>(log)),
|
||||
std::istreambuf_iterator<char>());
|
||||
out->output = tail_of(text, 4096);
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
#if defined(_WIN32)
|
||||
std::string command;
|
||||
for (std::size_t i = 0; i < argv.size(); ++i) {
|
||||
if (i != 0) {
|
||||
command.push_back(' ');
|
||||
}
|
||||
command += quote_argument(argv[i]);
|
||||
}
|
||||
auto widen = [](const std::string& text) {
|
||||
if (text.empty()) {
|
||||
return std::wstring();
|
||||
}
|
||||
const int size = MultiByteToWideChar(CP_UTF8, 0, text.c_str(),
|
||||
static_cast<int>(text.size()), nullptr, 0);
|
||||
std::wstring wide(static_cast<std::size_t>(size), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), static_cast<int>(text.size()), wide.data(),
|
||||
size);
|
||||
return wide;
|
||||
};
|
||||
const std::wstring wide_command = widen(command);
|
||||
const std::wstring wide_log = widen(fs_utf8::from_path(log_path));
|
||||
|
||||
SECURITY_ATTRIBUTES attributes{};
|
||||
attributes.nLength = sizeof(attributes);
|
||||
attributes.bInheritHandle = TRUE;
|
||||
// DELETE access plus FILE_FLAG_DELETE_ON_CLOSE means the log disappears when
|
||||
// the last handle goes away - including when this process is killed, which
|
||||
// would otherwise leave joc_process_*.log litter in the temp directory.
|
||||
HANDLE log_handle = CreateFileW(
|
||||
wide_log.c_str(), GENERIC_READ | GENERIC_WRITE | DELETE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &attributes, CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE, nullptr);
|
||||
if (log_handle == INVALID_HANDLE_VALUE) {
|
||||
return Status::fail(JOC_ERR_IO, stage::kOutput, "cannot create the process log file");
|
||||
}
|
||||
|
||||
// A delete-on-close file cannot be reopened by name (it is delete-pending), so
|
||||
// the child's output is read back through the handle it wrote to.
|
||||
auto read_log_handle = [&]() {
|
||||
LARGE_INTEGER start{};
|
||||
start.QuadPart = 0;
|
||||
if (!SetFilePointerEx(log_handle, start, nullptr, FILE_BEGIN)) {
|
||||
return;
|
||||
}
|
||||
std::string text;
|
||||
char buffer[1024];
|
||||
DWORD count = 0;
|
||||
while (ReadFile(log_handle, buffer, sizeof(buffer), &count, nullptr) && count > 0) {
|
||||
text.append(buffer, count);
|
||||
}
|
||||
out->output = tail_of(text, 4096);
|
||||
};
|
||||
|
||||
STARTUPINFOW startup{};
|
||||
startup.cb = sizeof(startup);
|
||||
startup.dwFlags = STARTF_USESTDHANDLES;
|
||||
startup.hStdOutput = log_handle;
|
||||
startup.hStdError = log_handle;
|
||||
startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
||||
PROCESS_INFORMATION process{};
|
||||
|
||||
std::vector<wchar_t> mutable_command(wide_command.begin(), wide_command.end());
|
||||
mutable_command.push_back(L'\0');
|
||||
const BOOL started = CreateProcessW(nullptr, mutable_command.data(), nullptr, nullptr, TRUE,
|
||||
CREATE_NO_WINDOW, nullptr, nullptr, &startup, &process);
|
||||
if (!started) {
|
||||
CloseHandle(log_handle); // delete-on-close removes the file
|
||||
return Status::fail(JOC_ERR_LIBRARY_MISSING, stage::kOutput,
|
||||
"cannot start " + argv[0] + " (is it on PATH?)");
|
||||
}
|
||||
WaitForSingleObject(process.hProcess, INFINITE);
|
||||
DWORD exit_code = 0;
|
||||
GetExitCodeProcess(process.hProcess, &exit_code);
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
// Read the log before the delete-on-close handle goes away.
|
||||
read_log_handle();
|
||||
CloseHandle(log_handle);
|
||||
out->exit_code = static_cast<std::uint32_t>(exit_code);
|
||||
#else
|
||||
std::string command;
|
||||
for (std::size_t i = 0; i < argv.size(); ++i) {
|
||||
if (i != 0) {
|
||||
command.push_back(' ');
|
||||
}
|
||||
command += quote_argument(argv[i]);
|
||||
}
|
||||
command += " > " + quote_argument(fs_utf8::from_path(log_path)) + " 2>&1";
|
||||
const int status = std::system(command.c_str());
|
||||
// system() reports a wait status, not the child's exit code.
|
||||
out->exit_code = status == -1 ? 127u
|
||||
: WIFEXITED(status) ? static_cast<std::uint32_t>(WEXITSTATUS(status))
|
||||
: 128u;
|
||||
read_log();
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove(log_path, ignored);
|
||||
#endif
|
||||
|
||||
if (out->exit_code != 0) {
|
||||
return Status::fail(JOC_ERR_INPUT_FORMAT, stage::kOutput,
|
||||
argv[0] + " failed with exit code " + std::to_string(out->exit_code) +
|
||||
(out->output.empty() ? "" : ": " + tail_of(out->output, 400)));
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
} // namespace joc::io
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "foundation/status.h"
|
||||
|
||||
namespace joc::io {
|
||||
|
||||
struct ProcessResult {
|
||||
std::uint32_t exit_code = 0;
|
||||
std::string output;
|
||||
};
|
||||
|
||||
Status run_process(const std::vector<std::string>& argv, ProcessResult* out);
|
||||
|
||||
} // namespace joc::io
|
||||
@@ -0,0 +1,242 @@
|
||||
#include "io/wav_writer.h"
|
||||
#include "foundation/fs_utf8.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace joc::io {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint16_t kWaveFormatPcm = 0x0001;
|
||||
constexpr std::uint16_t kWaveFormatIeeeFloat = 0x0003;
|
||||
constexpr std::uint16_t kWaveFormatExtensible = 0xFFFE;
|
||||
constexpr std::uint8_t kPcmGuid[16] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
|
||||
0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71};
|
||||
constexpr std::uint8_t kFloatGuid[16] = {0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
|
||||
0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71};
|
||||
|
||||
void put_u16(std::string* out, std::uint16_t value) {
|
||||
char buffer[2];
|
||||
std::memcpy(buffer, &value, 2);
|
||||
out->append(buffer, 2);
|
||||
}
|
||||
|
||||
void put_u32(std::string* out, std::uint32_t value) {
|
||||
char buffer[4];
|
||||
std::memcpy(buffer, &value, 4);
|
||||
out->append(buffer, 4);
|
||||
}
|
||||
|
||||
void put_u64(std::string* out, std::uint64_t value) {
|
||||
char buffer[8];
|
||||
std::memcpy(buffer, &value, 8);
|
||||
out->append(buffer, 8);
|
||||
}
|
||||
|
||||
// Port of speaker_wav._fmt_chunk.
|
||||
std::string fmt_chunk(std::uint32_t channels, std::uint32_t rate, SampleFormat format, WavInfo* info) {
|
||||
std::uint16_t simple_tag = 0;
|
||||
const std::uint8_t* guid = nullptr;
|
||||
if (format == SampleFormat::Float32) {
|
||||
info->bits_per_sample = 32;
|
||||
info->bytes_per_sample = 4;
|
||||
simple_tag = kWaveFormatIeeeFloat;
|
||||
guid = kFloatGuid;
|
||||
} else {
|
||||
info->bits_per_sample = 24;
|
||||
info->bytes_per_sample = 3;
|
||||
simple_tag = kWaveFormatPcm;
|
||||
guid = kPcmGuid;
|
||||
}
|
||||
const std::uint32_t block_align = channels * info->bytes_per_sample;
|
||||
const std::uint32_t byte_rate = rate * block_align;
|
||||
info->block_align = block_align;
|
||||
|
||||
std::string body;
|
||||
if (channels <= 2) {
|
||||
put_u16(&body, simple_tag);
|
||||
put_u16(&body, static_cast<std::uint16_t>(channels));
|
||||
put_u32(&body, rate);
|
||||
put_u32(&body, byte_rate);
|
||||
put_u16(&body, static_cast<std::uint16_t>(block_align));
|
||||
put_u16(&body, static_cast<std::uint16_t>(info->bits_per_sample));
|
||||
} else {
|
||||
put_u16(&body, kWaveFormatExtensible);
|
||||
put_u16(&body, static_cast<std::uint16_t>(channels));
|
||||
put_u32(&body, rate);
|
||||
put_u32(&body, byte_rate);
|
||||
put_u16(&body, static_cast<std::uint16_t>(block_align));
|
||||
put_u16(&body, static_cast<std::uint16_t>(info->bits_per_sample));
|
||||
put_u16(&body, 22);
|
||||
put_u16(&body, static_cast<std::uint16_t>(info->bits_per_sample));
|
||||
put_u32(&body, 0);
|
||||
body.append(reinterpret_cast<const char*>(guid), 16);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
// int32 conversion identical to NumPy's float32 -> int32 cast after clipping.
|
||||
std::int32_t to_int32(const float value) {
|
||||
if (!std::isfinite(value)) {
|
||||
return std::numeric_limits<std::int32_t>::min();
|
||||
}
|
||||
return static_cast<std::int32_t>(value);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void pack_int24(const float* interleaved, std::size_t frames, std::size_t channels,
|
||||
std::string* out) {
|
||||
const std::size_t count = frames * channels;
|
||||
out->resize(count * 3);
|
||||
char* target = out->data();
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
float value = interleaved[i];
|
||||
if (value > 1.0f) {
|
||||
value = 1.0f;
|
||||
} else if (value < -1.0f) {
|
||||
value = -1.0f;
|
||||
}
|
||||
const std::int32_t scaled = to_int32(value * 8388607.0f);
|
||||
const std::uint32_t bits = static_cast<std::uint32_t>(scaled);
|
||||
target[i * 3 + 0] = static_cast<char>(bits & 0xFFu);
|
||||
target[i * 3 + 1] = static_cast<char>((bits >> 8) & 0xFFu);
|
||||
target[i * 3 + 2] = static_cast<char>((bits >> 16) & 0xFFu);
|
||||
}
|
||||
}
|
||||
|
||||
WavWriter::~WavWriter() {
|
||||
if (file_ != nullptr) {
|
||||
std::fclose(file_);
|
||||
file_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Status WavWriter::open(const std::string& path, std::uint32_t channels, std::uint32_t rate,
|
||||
SampleFormat format, std::uint64_t total_frames) {
|
||||
if (channels == 0 || rate == 0) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOutput,
|
||||
"WAV writer needs a positive channel count and rate");
|
||||
}
|
||||
path_ = path;
|
||||
channels_ = channels;
|
||||
total_frames_ = total_frames;
|
||||
frames_written_ = 0;
|
||||
finalized_ = false;
|
||||
info_ = WavInfo{};
|
||||
info_.format = format;
|
||||
|
||||
const std::string fmt = fmt_chunk(channels, rate, format, &info_);
|
||||
const std::uint64_t data_size = total_frames * info_.block_align;
|
||||
info_.data_bytes = data_size;
|
||||
const std::uint64_t riff_file_size = 12u + 8u + fmt.size() + 8u + data_size;
|
||||
const bool rf64 = (riff_file_size - 8u) > 0xFFFFFFFFull;
|
||||
info_.rf64 = rf64;
|
||||
|
||||
std::string header;
|
||||
if (rf64) {
|
||||
const std::uint64_t file_size = 12u + 36u + 8u + fmt.size() + 8u + data_size;
|
||||
header.append("RF64", 4);
|
||||
put_u32(&header, 0xFFFFFFFFu);
|
||||
header.append("WAVE", 4);
|
||||
header.append("ds64", 4);
|
||||
put_u32(&header, 28);
|
||||
put_u64(&header, file_size - 8u);
|
||||
put_u64(&header, data_size);
|
||||
put_u64(&header, total_frames);
|
||||
put_u32(&header, 0);
|
||||
} else {
|
||||
header.append("RIFF", 4);
|
||||
put_u32(&header, static_cast<std::uint32_t>(riff_file_size - 8u));
|
||||
header.append("WAVE", 4);
|
||||
}
|
||||
header.append("fmt ", 4);
|
||||
put_u32(&header, static_cast<std::uint32_t>(fmt.size()));
|
||||
header.append(fmt);
|
||||
header.append("data", 4);
|
||||
put_u32(&header, rf64 ? 0xFFFFFFFFu : static_cast<std::uint32_t>(data_size));
|
||||
|
||||
file_ = fs_utf8::fopen(path, "wb");
|
||||
if (file_ == nullptr) {
|
||||
// The reference creates the parent directory itself.
|
||||
std::error_code ignored;
|
||||
const std::filesystem::path parent = std::filesystem::path(path).parent_path();
|
||||
if (!parent.empty()) {
|
||||
std::filesystem::create_directories(parent, ignored);
|
||||
}
|
||||
file_ = fs_utf8::fopen(path, "wb");
|
||||
}
|
||||
if (file_ == nullptr) {
|
||||
return Status::fail(JOC_ERR_OUTPUT_OPEN, stage::kOutput, "cannot open " + path);
|
||||
}
|
||||
if (std::fwrite(header.data(), 1, header.size(), file_) != header.size()) {
|
||||
std::fclose(file_);
|
||||
file_ = nullptr;
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "cannot write header to " + path);
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status WavWriter::write(const double* interleaved, std::size_t frames) {
|
||||
if (file_ == nullptr) {
|
||||
return Status::fail(JOC_ERR_STATE, stage::kOutput, "WAV writer is not open");
|
||||
}
|
||||
if (frames == 0) {
|
||||
return Status::success();
|
||||
}
|
||||
if (frames_written_ + frames > total_frames_) {
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput,
|
||||
"WAV writer received more frames than the header declared (declared " +
|
||||
std::to_string(total_frames_) + ", written " +
|
||||
std::to_string(frames_written_) + ", requested " +
|
||||
std::to_string(frames) + ")");
|
||||
}
|
||||
const std::size_t count = frames * channels_;
|
||||
if (info_.format == SampleFormat::Float32) {
|
||||
std::vector<float> converted(count);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
converted[i] = static_cast<float>(interleaved[i]);
|
||||
}
|
||||
if (std::fwrite(converted.data(), sizeof(float), count, file_) != count) {
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "write failed for " + path_);
|
||||
}
|
||||
} else {
|
||||
std::vector<float> converted(count);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
converted[i] = static_cast<float>(interleaved[i]);
|
||||
}
|
||||
std::string packed;
|
||||
pack_int24(converted.data(), frames, channels_, &packed);
|
||||
if (std::fwrite(packed.data(), 1, packed.size(), file_) != packed.size()) {
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "write failed for " + path_);
|
||||
}
|
||||
}
|
||||
frames_written_ += frames;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status WavWriter::finalize() {
|
||||
if (file_ == nullptr) {
|
||||
return Status::fail(JOC_ERR_STATE, stage::kOutput, "WAV writer is not open");
|
||||
}
|
||||
if (frames_written_ != total_frames_) {
|
||||
std::fclose(file_);
|
||||
file_ = nullptr;
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput,
|
||||
"WAV writer wrote " + std::to_string(frames_written_) + " of " +
|
||||
std::to_string(total_frames_) + " frames");
|
||||
}
|
||||
const int result = std::fclose(file_);
|
||||
file_ = nullptr;
|
||||
finalized_ = true;
|
||||
if (result != 0) {
|
||||
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "close failed for " + path_);
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
} // namespace joc::io
|
||||
@@ -0,0 +1,61 @@
|
||||
// Port of src/speaker_wav.py.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include "foundation/status.h"
|
||||
#include "joc_core.h"
|
||||
|
||||
namespace joc::io {
|
||||
|
||||
enum class SampleFormat { Float32, Int24 };
|
||||
|
||||
struct WavInfo {
|
||||
SampleFormat format = SampleFormat::Float32;
|
||||
std::uint32_t bits_per_sample = 32;
|
||||
std::uint32_t bytes_per_sample = 4;
|
||||
std::uint32_t block_align = 0;
|
||||
std::uint64_t data_bytes = 0;
|
||||
bool rf64 = false;
|
||||
};
|
||||
|
||||
// int24 packing shared by the WAV and ADM writers:
|
||||
// trunc(clip(v, -1, 1) * 8388607.0f) with the low three bytes written LE.
|
||||
// NaN follows NumPy's float->int cast (INT32_MIN) so that the C++ conversion is
|
||||
// never undefined; the reference passes it through unguarded (plan TD-3.11).
|
||||
void pack_int24(const float* interleaved, std::size_t frames, std::size_t channels,
|
||||
std::string* out);
|
||||
|
||||
class WavWriter {
|
||||
public:
|
||||
WavWriter() = default;
|
||||
~WavWriter();
|
||||
|
||||
WavWriter(const WavWriter&) = delete;
|
||||
WavWriter& operator=(const WavWriter&) = delete;
|
||||
|
||||
// `total_frames` must be known up front: the header depends on it.
|
||||
Status open(const std::string& path, std::uint32_t channels, std::uint32_t rate,
|
||||
SampleFormat format, std::uint64_t total_frames);
|
||||
|
||||
Status write(const double* interleaved, std::size_t frames);
|
||||
|
||||
Status finalize();
|
||||
|
||||
const WavInfo& info() const { return info_; }
|
||||
std::uint64_t frames_written() const { return frames_written_; }
|
||||
|
||||
private:
|
||||
std::FILE* file_ = nullptr;
|
||||
std::string path_;
|
||||
WavInfo info_;
|
||||
std::uint32_t channels_ = 0;
|
||||
std::uint64_t total_frames_ = 0;
|
||||
std::uint64_t frames_written_ = 0;
|
||||
bool finalized_ = false;
|
||||
};
|
||||
|
||||
} // namespace joc::io
|
||||
@@ -0,0 +1,215 @@
|
||||
#include "io/zip_reader.h"
|
||||
#include "foundation/fs_utf8.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "io/inflate.h"
|
||||
|
||||
namespace joc::io {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t kLocalHeaderSignature = 0x04034b50u;
|
||||
constexpr std::uint32_t kCentralHeaderSignature = 0x02014b50u;
|
||||
constexpr std::uint32_t kEndOfCentralDirectory = 0x06054b50u;
|
||||
|
||||
std::uint16_t read_u16(const std::uint8_t* p) {
|
||||
return static_cast<std::uint16_t>(p[0] | (p[1] << 8));
|
||||
}
|
||||
|
||||
std::uint32_t read_u32(const std::uint8_t* p) {
|
||||
return static_cast<std::uint32_t>(p[0]) | (static_cast<std::uint32_t>(p[1]) << 8) |
|
||||
(static_cast<std::uint32_t>(p[2]) << 16) | (static_cast<std::uint32_t>(p[3]) << 24);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::uint32_t crc32_of(const std::uint8_t* data, std::size_t size) {
|
||||
static std::uint32_t table[256];
|
||||
static bool ready = false;
|
||||
if (!ready) {
|
||||
for (std::uint32_t i = 0; i < 256; ++i) {
|
||||
std::uint32_t value = i;
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
value = (value & 1u) ? (0xEDB88320u ^ (value >> 1)) : (value >> 1);
|
||||
}
|
||||
table[i] = value;
|
||||
}
|
||||
ready = true;
|
||||
}
|
||||
std::uint32_t crc = 0xFFFFFFFFu;
|
||||
for (std::size_t i = 0; i < size; ++i) {
|
||||
crc = table[(crc ^ data[i]) & 0xFFu] ^ (crc >> 8);
|
||||
}
|
||||
return crc ^ 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
bool ZipArchive::open(const std::string& path, std::string* error) {
|
||||
entries_.clear();
|
||||
data_.clear();
|
||||
std::FILE* file = fs_utf8::fopen(path, "rb");
|
||||
if (file == nullptr) {
|
||||
if (error != nullptr) {
|
||||
*error = "cannot open " + path;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
std::fseek(file, 0, SEEK_END);
|
||||
const long long size = std::ftell(file);
|
||||
std::fseek(file, 0, SEEK_SET);
|
||||
if (size <= 0) {
|
||||
std::fclose(file);
|
||||
if (error != nullptr) {
|
||||
*error = "empty file " + path;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
data_.resize(static_cast<std::size_t>(size));
|
||||
const std::size_t got = std::fread(data_.data(), 1, data_.size(), file);
|
||||
std::fclose(file);
|
||||
if (got != data_.size()) {
|
||||
if (error != nullptr) {
|
||||
*error = "short read on " + path;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t eocd = std::string::npos;
|
||||
const std::size_t scan_start = data_.size() > 65557u ? data_.size() - 65557u : 0u;
|
||||
for (std::size_t i = data_.size(); i-- > scan_start;) {
|
||||
if (i + 4u <= data_.size() && read_u32(&data_[i]) == kEndOfCentralDirectory) {
|
||||
eocd = i;
|
||||
break;
|
||||
}
|
||||
if (i == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (eocd == std::string::npos || eocd + 22u > data_.size()) {
|
||||
if (error != nullptr) {
|
||||
*error = "not a zip archive (no end-of-central-directory)";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const std::uint16_t entry_count = read_u16(&data_[eocd + 10]);
|
||||
const std::uint32_t directory_offset = read_u32(&data_[eocd + 16]);
|
||||
if (directory_offset >= data_.size()) {
|
||||
if (error != nullptr) {
|
||||
*error = "central directory offset out of range";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
std::size_t cursor = directory_offset;
|
||||
for (std::uint16_t index = 0; index < entry_count; ++index) {
|
||||
if (cursor + 46u > data_.size() || read_u32(&data_[cursor]) != kCentralHeaderSignature) {
|
||||
if (error != nullptr) {
|
||||
*error = "malformed central directory entry " + std::to_string(index);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
ZipEntry entry;
|
||||
entry.method = read_u16(&data_[cursor + 10]);
|
||||
entry.crc32 = read_u32(&data_[cursor + 16]);
|
||||
entry.compressed_size = read_u32(&data_[cursor + 20]);
|
||||
entry.uncompressed_size = read_u32(&data_[cursor + 24]);
|
||||
const std::uint16_t name_length = read_u16(&data_[cursor + 28]);
|
||||
const std::uint16_t extra_length = read_u16(&data_[cursor + 30]);
|
||||
const std::uint16_t comment_length = read_u16(&data_[cursor + 32]);
|
||||
entry.local_header_offset = read_u32(&data_[cursor + 42]);
|
||||
if (entry.compressed_size == 0xFFFFFFFFu || entry.uncompressed_size == 0xFFFFFFFFu ||
|
||||
entry.local_header_offset == 0xFFFFFFFFu) {
|
||||
if (error != nullptr) {
|
||||
*error = "zip64 archives are not supported";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (cursor + 46u + name_length > data_.size()) {
|
||||
if (error != nullptr) {
|
||||
*error = "member name out of range";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
entry.name.assign(reinterpret_cast<const char*>(&data_[cursor + 46]), name_length);
|
||||
entries_.push_back(std::move(entry));
|
||||
cursor += 46u + name_length + extra_length + comment_length;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const ZipEntry* ZipArchive::find(const std::string& name) const {
|
||||
for (const ZipEntry& entry : entries_) {
|
||||
if (entry.name == name) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ZipArchive::extract(const ZipEntry& entry, std::vector<std::uint8_t>* out,
|
||||
std::string* error) const {
|
||||
if (out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const std::size_t offset = entry.local_header_offset;
|
||||
if (offset + 30u > data_.size() || read_u32(&data_[offset]) != kLocalHeaderSignature) {
|
||||
if (error != nullptr) {
|
||||
*error = "bad local header for " + entry.name;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const std::uint16_t name_length = read_u16(&data_[offset + 26]);
|
||||
const std::uint16_t extra_length = read_u16(&data_[offset + 28]);
|
||||
const std::size_t start = offset + 30u + name_length + extra_length;
|
||||
if (start + entry.compressed_size > data_.size()) {
|
||||
if (error != nullptr) {
|
||||
*error = "member data out of range for " + entry.name;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (entry.method == 0u) {
|
||||
out->assign(data_.begin() + static_cast<std::ptrdiff_t>(start),
|
||||
data_.begin() + static_cast<std::ptrdiff_t>(start + entry.compressed_size));
|
||||
} else if (entry.method == 8u) {
|
||||
if (!inflate_raw(&data_[start], entry.compressed_size, out)) {
|
||||
if (error != nullptr) {
|
||||
*error = "deflate error in " + entry.name;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (error != nullptr) {
|
||||
*error = "unsupported compression method " + std::to_string(entry.method) + " for " +
|
||||
entry.name;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (entry.uncompressed_size != 0u && out->size() != entry.uncompressed_size) {
|
||||
if (error != nullptr) {
|
||||
*error = "size mismatch for " + entry.name + " (" + std::to_string(out->size()) +
|
||||
" vs " + std::to_string(entry.uncompressed_size) + ")";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (entry.crc32 != 0u && crc32_of(out->data(), out->size()) != entry.crc32) {
|
||||
if (error != nullptr) {
|
||||
*error = "CRC mismatch for " + entry.name;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ZipArchive::read_member(const std::string& name, std::vector<std::uint8_t>* out,
|
||||
std::string* error) const {
|
||||
const ZipEntry* entry = find(name);
|
||||
if (entry == nullptr) {
|
||||
if (error != nullptr) {
|
||||
*error = "member not found: " + name;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return extract(*entry, out, error);
|
||||
}
|
||||
|
||||
} // namespace joc::io
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace joc::io {
|
||||
|
||||
struct ZipEntry {
|
||||
std::string name;
|
||||
std::uint16_t method = 0;
|
||||
std::uint32_t crc32 = 0;
|
||||
std::uint32_t compressed_size = 0;
|
||||
std::uint32_t uncompressed_size = 0;
|
||||
std::uint32_t local_header_offset = 0;
|
||||
};
|
||||
|
||||
class ZipArchive {
|
||||
public:
|
||||
bool open(const std::string& path, std::string* error);
|
||||
|
||||
const std::vector<ZipEntry>& entries() const { return entries_; }
|
||||
|
||||
const ZipEntry* find(const std::string& name) const;
|
||||
|
||||
bool extract(const ZipEntry& entry, std::vector<std::uint8_t>* out, std::string* error) const;
|
||||
|
||||
bool read_member(const std::string& name, std::vector<std::uint8_t>* out, std::string* error) const;
|
||||
|
||||
private:
|
||||
std::vector<std::uint8_t> data_;
|
||||
std::vector<ZipEntry> entries_;
|
||||
};
|
||||
|
||||
std::uint32_t crc32_of(const std::uint8_t* data, std::size_t size);
|
||||
|
||||
} // namespace joc::io
|
||||
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
// JOC Huffman trees required by the bitstream parser.
|
||||
|
||||
static const int joc_huff_code_coarse_generic[][2] =
|
||||
{
|
||||
{ -1, 1}, { 2, -2}, { -96, 3}, { 4, -3}, { -95, 5}, { 6, 7}, { -4, -94}, { 8, 9}, { -5, -93}, { 10, 11},
|
||||
{ -6, -92}, { 12, 13}, { -7, -91}, { 14, 15}, { 16, -90}, { -8, 17}, { 18, -89}, { -9, 19}, { 20, 21}, { -88, -10},
|
||||
{ 22, 23}, { -11, -87}, { 24, 25}, { 26, -86}, { -12, 27}, { 28, -85}, { -13, 29}, { 30, 31}, { 32, -84}, { -14, 33},
|
||||
{ 34, -15}, { -83, 35}, { 36, 37}, { -16, 38}, { -17, -82}, { 39, 40}, { 41, -81}, { 42, 43}, { 44, 45}, { 46, 47},
|
||||
{ 48, 49}, { 50, 51}, { 52, -18}, { -78, 53}, { -19, 54}, { 55, 56}, { 57, 58}, { -22, 59}, { 60, 61}, { 62, 63},
|
||||
{ 64, 65}, { 66, 67}, { 68, -20}, { -21, -79}, { -80, -25}, { 69, 70}, { -26, 71}, { 72, 73}, { 74, 75}, { 76, 77},
|
||||
{ 78, 79}, { 80, 81}, { 82, 83}, { 84, 85}, { 86, 87}, { 88, 89}, { 90, 91}, { 92, 93}, { 94, -23}, { -74, -75},
|
||||
{ -72, -73}, { -76, -77}, { -34, -35}, { -32, -33}, { -38, -39}, { -36, -37}, { -30, -31}, { -28, -29}, { -50, -51}, { -48, -49},
|
||||
{ -54, -55}, { -52, -53}, { -42, -43}, { -40, -41}, { -46, -47}, { -44, -45}, { -66, -67}, { -64, -65}, { -70, -71}, { -68, -69},
|
||||
{ -58, -59}, { -56, -57}, { -62, -63}, { -60, -61}, { -24, -27}
|
||||
};
|
||||
|
||||
static const int joc_huff_code_fine_generic[][2] =
|
||||
{
|
||||
{ -1, 1}, { 2, 3}, { -2,-192}, { 4, 5}, { 6, -3}, {-191, 7}, { 8, 9}, { -4,-190}, { 10, 11}, { -5,-189},
|
||||
{ 12, 13}, { -6, 14}, {-188, 15}, { 16, -7}, {-187, 17}, { 18, -8}, {-186, 19}, { 20, -9}, {-185, 21}, { 22, -10},
|
||||
{-184, 23}, { 24, -11}, { 25,-183}, { 26, 27}, { -12,-182}, { 28, 29}, { -13,-181}, { 30, 31}, {-180, -14}, { 32, 33},
|
||||
{ 34,-179}, { -15, 35}, { 36,-178}, { -16, 37}, { 38,-177}, { 39, -17}, { 40, 41}, {-176, 42}, { -18, 43}, { -19, 44},
|
||||
{-175, 45}, { 46,-174}, { -20, 47}, {-173, 48}, { 49, -21}, { 50, 51}, { 52, -22}, { 53, 54}, {-172, 55}, {-171, -23},
|
||||
{ 56, 57}, { 58,-170}, { 59, -24}, { -25, 60}, {-169, 61}, { 62, 63}, { 64, 65}, { 66, 67}, {-168, 68}, { -26, 69},
|
||||
{-167, -27}, { 70,-166}, {-165, 71}, { -29, 72}, { 73, 74}, { -30, 75}, { 76, 77}, { 78, 79}, { 80, -28}, { 81, 82},
|
||||
{ 83,-163}, { -31, -33}, {-164,-161}, { 84, 85}, { 86, 87}, { 88, 89}, { 90, 91}, { 92, 93}, { 94, 95}, { 96, 97},
|
||||
{ 98, 99}, { -32,-162}, { 100, 101}, { 102, 103}, { 104, 105}, { 106, 107}, { 108, 109}, { 110, 111}, {-160, 112}, { -36, -38},
|
||||
{ 113, 114}, { 115, 116}, { 117, 118}, { 119, 120}, { 121, 122}, { 123, 124}, { 125, 126}, { 127, 128}, { 129, 130}, { 131, 132},
|
||||
{ 133, -35}, {-158, 134}, {-155,-156}, { -37, -42}, { 135, 136}, { 137, 138}, { 139, 140}, { 141, 142}, { 143, 144}, { 145, 146},
|
||||
{ 147, 148}, { 149, 150}, { 151, 152}, { 153, 154}, { 155, 156}, { 157, 158}, { 159, 160}, { 161, 162}, { 163, 164}, { 165, 166},
|
||||
{ 167, 168}, { 169, 170}, { 171, 172}, { 173, 174}, { 175, 176}, { 177, 178}, { 179, 180}, { 181, 182}, { 183, 184}, {-157, 185},
|
||||
{ -45, -48}, { 186, 187}, { 188, 189}, { -34, -41}, { 190, -39}, { -60, -61}, { -58, -59}, { -64, -65}, { -62, -63}, { -52, -53},
|
||||
{ -50, -51}, { -56, -57}, { -54, -55}, { -76, -77}, { -74, -75}, { -80, -81}, { -78, -79}, { -68, -69}, { -66, -67}, { -72, -73},
|
||||
{ -70, -71}, { -47, -49}, { -44, -46}, {-124,-125}, {-122,-123}, {-128,-129}, {-126,-127}, {-116,-117}, {-114,-115}, {-120,-121},
|
||||
{-118,-119}, {-140,-141}, {-138,-139}, {-144,-145}, {-142,-143}, {-132,-133}, {-130,-131}, {-136,-137}, {-134,-135}, { -92, -93},
|
||||
{ -90, -91}, { -96, -97}, { -94, -95}, { -84, -85}, { -82, -83}, { -88, -89}, { -86, -87}, {-108,-109}, {-106,-107}, {-112,-113},
|
||||
{-110,-111}, {-100,-101}, { -98, -99}, {-104,-105}, {-102,-103}, {-154,-159}, {-148,-149}, {-146,-147}, {-152,-153}, {-150,-151},
|
||||
{ -40, -43}
|
||||
};
|
||||
|
||||
static const int joc_huff_code_coarse_coeff_sparse[][2] =
|
||||
{
|
||||
{ -1, 1}, { 2, 3}, { -2, -96}, { 4, 5}, { 6, -95}, { -3, 7}, { 8, 9}, { -4, 10}, { -94, 11}, { 12, -5},
|
||||
{ -93, 13}, { 14, 15}, { -6, -92}, { 16, 17}, { 18, -7}, { -91, 19}, { 20, -8}, { -90, 21}, { 22, 23}, { -9, -89},
|
||||
{ 24, 25}, { 26, -10}, { -88, 27}, { 28, 29}, { 30, -11}, { -87, 31}, { 32, 33}, { 34, 35}, { -12, -86}, { 36, 37},
|
||||
{ 38, -13}, { 39, -85}, { 40, 41}, { 42, 43}, { -14, -84}, { 44, 45}, { 46, 47}, { -83, -15}, { 48, 49}, { 50, -16},
|
||||
{ 51, 52}, { -82, 53}, { 54, -81}, { 55, 56}, { -17, 57}, { 58, -80}, { 59, 60}, { -18, 61}, { 62, 63}, { -79, 64},
|
||||
{ -19, -78}, { 65, 66}, { 67, 68}, { 69, -20}, { -77, -21}, { 70, 71}, { 72, 73}, { 74, -76}, { 75, -22}, { 76, 77},
|
||||
{ -75, 78}, { 79, 80}, { -54, -74}, { -73, 81}, { -23, 82}, { -50, -24}, { -55, -25}, { 83, -47}, { -49, -44}, { -71, 84},
|
||||
{ -48, -51}, { 85, -72}, { -26, -53}, { -70, -27}, { 86, -45}, { 87, 88}, { -68, 89}, { -29, -43}, { 90, -30}, { -46, -69},
|
||||
{ 91, -28}, { -52, -31}, { 92, -32}, { 93, -64}, { -67, 94}, { -36, -33}, { -63, -37}, { -65, -61}, { -66, -59}, { -34, -38},
|
||||
{ -41, -42}, { -35, -60}, { -39, -57}, { -56, -40}, { -62, -58}
|
||||
};
|
||||
|
||||
static const int joc_huff_code_fine_coeff_sparse[][2] =
|
||||
{
|
||||
{ 1, -1}, { 2, 3}, { 4, -2}, {-192, 5}, { 6, 7}, { 8, -3}, {-191, 9}, { 10, 11}, { 12,-190}, { -4, 13},
|
||||
{ 14, 15}, {-189, -5}, { 16, 17}, { 18, -6}, {-188, 19}, { 20, 21}, { -7,-187}, { 22, 23}, { -8, 24}, {-186, 25},
|
||||
{ -9, 26}, { 27,-185}, { 28, -10}, { 29, 30}, {-184, 31}, { -11, 32}, { 33,-183}, { 34, -12}, { 35,-182}, { 36, 37},
|
||||
{ 38, -13}, {-181, 39}, { 40, -14}, { 41,-180}, { 42, 43}, {-179, -15}, { 44, -16}, { 45,-178}, { 46, 47}, { 48, 49},
|
||||
{ 50,-177}, { -17, 51}, { -18, 52}, {-176, 53}, { 54, 55}, {-175, -19}, { 56, 57}, { 58, -20}, { 59,-174}, { 60, 61},
|
||||
{ -21, 62}, { 63,-173}, { 64, 65}, { 66,-172}, { 67, 68}, { -22, 69}, { 70, 71}, { -23, 72}, {-171, 73}, { 74, 75},
|
||||
{ 76, -24}, { 77,-170}, { -25, 78}, { 79, 80}, { 81,-169}, { 82, 83}, { 84, -26}, { 85,-168}, { 86, 87}, { 88, 89},
|
||||
{-167, 90}, { -27, 91}, { 92, -28}, { 93,-166}, { 94, -29}, { 95, 96}, { 97, 98}, {-165, 99}, { 100, -30}, {-164, 101},
|
||||
{ 102, 103}, { 104, 105}, {-163, 106}, { -31, 107}, { -32, 108}, { 109, 110}, {-161, 111}, {-160,-162}, { 112, -34}, { -33, 113},
|
||||
{ 114, 115}, { 116, 117}, { 118, 119}, { 120,-159}, { 121, 122}, { 123,-158}, { 124, 125}, { -36,-155}, { 126, 127}, { -35, 128},
|
||||
{ 129, 130}, {-157, 131}, {-156, 132}, { -37, 133}, { 134, 135}, {-154, -38}, { 136, 137}, { -39, -41}, { 138,-153}, { 139, -40},
|
||||
{-149, 140}, { 141, 142}, { 143, 144}, {-151, 145}, { 146, 147}, { 148, -42}, { -43, 149}, { 150, 151}, {-152, 152}, { -46, -98},
|
||||
{ 153, 154}, { 155,-147}, { 156, 157}, { 158,-107}, { 159, 160}, {-145,-150}, { -96, 161}, { 162, -45}, {-146, 163}, { 164, -97},
|
||||
{-108,-105}, {-148,-106}, { -44, 165}, { -94,-141}, { -99, 166}, { -89, 167}, { -50, -95}, {-100, -48}, {-144, 168}, { 169, 170},
|
||||
{ -51,-142}, { -90, -91}, { -47, -49}, { 171, -53}, { -93,-143}, {-137,-138}, { -55,-101}, { 172, 173}, { -54, -86}, { -88, -87},
|
||||
{-103, 174}, { 175, -61}, {-109, 176}, { 177, 178}, { -52,-139}, { -57,-140}, { 179, 180}, { -56,-136}, { -58,-102}, { 181, 182},
|
||||
{ -60,-135}, { 183,-104}, {-128,-134}, { -92, 184}, { -59, -62}, { 185, 186}, { -71,-133}, { 187,-127}, {-126, 188}, { -63, -64},
|
||||
{ -85,-132}, { 189, -66}, {-121,-125}, { 190, -68}, { -74, -75}, { -70, -73}, { -81, -65}, {-118,-131}, { -72,-110}, {-119,-120},
|
||||
{ -76, -84}, {-122,-130}, { -83,-117}, { -69, -78}, { -80, -82}, {-123,-124}, { -67,-116}, {-129, -77}, {-113,-114}, {-112,-115},
|
||||
{ -79,-111}
|
||||
};
|
||||
|
||||
static const int joc_huff_code_5ch_pos_index_sparse[][2] =
|
||||
{
|
||||
{ -1, 1}, { 2, 3}, { -4, -3}, { -2, -5}
|
||||
};
|
||||
|
||||
static const int joc_huff_code_7ch_pos_index_sparse[][2] =
|
||||
{
|
||||
{ -1, 1}, { 2, 3}, { 4, 5}, { -4, -3}, { -2, -5}, { -6, -7}
|
||||
};
|
||||
@@ -0,0 +1,415 @@
|
||||
#include "joc_bitstream/joc_parser.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "foundation/bit_reader.h"
|
||||
#include "joc_huffman_tables.h"
|
||||
|
||||
namespace joc::joc {
|
||||
|
||||
namespace {
|
||||
|
||||
struct NumChannelsEntry {
|
||||
std::uint32_t config;
|
||||
std::int16_t channels;
|
||||
};
|
||||
constexpr NumChannelsEntry kNumChannels[] = {
|
||||
{0u, 5}, {1u, 7}, {2u, 7}, {3u, 5}, {4u, 7},
|
||||
};
|
||||
|
||||
struct NumBandsEntry {
|
||||
std::uint32_t index;
|
||||
std::int16_t bands;
|
||||
};
|
||||
constexpr NumBandsEntry kNumBands[] = {
|
||||
{0u, 1}, {1u, 3}, {2u, 5}, {3u, 7}, {4u, 9}, {5u, 12}, {6u, 15}, {7u, 23},
|
||||
};
|
||||
|
||||
// Floored modulo: Python's % operator semantics, so that the ported
|
||||
inline std::int64_t floored_mod(std::int64_t value, std::int64_t modulus) {
|
||||
const std::int64_t remainder = value % modulus;
|
||||
return remainder < 0 ? remainder + modulus : remainder;
|
||||
}
|
||||
|
||||
enum class SymbolKind { Mtx, Idx, Vec };
|
||||
|
||||
struct Tree {
|
||||
const int (*nodes)[2] = nullptr;
|
||||
int count = 0;
|
||||
};
|
||||
|
||||
Tree select_tree(std::uint32_t quant_idx, SymbolKind kind, int n_channels) {
|
||||
Tree tree;
|
||||
switch (kind) {
|
||||
case SymbolKind::Idx:
|
||||
if (n_channels == 5) {
|
||||
tree.nodes = joc_huff_code_5ch_pos_index_sparse;
|
||||
tree.count = static_cast<int>(sizeof(joc_huff_code_5ch_pos_index_sparse) /
|
||||
sizeof(joc_huff_code_5ch_pos_index_sparse[0]));
|
||||
} else {
|
||||
tree.nodes = joc_huff_code_7ch_pos_index_sparse;
|
||||
tree.count = static_cast<int>(sizeof(joc_huff_code_7ch_pos_index_sparse) /
|
||||
sizeof(joc_huff_code_7ch_pos_index_sparse[0]));
|
||||
}
|
||||
break;
|
||||
case SymbolKind::Vec:
|
||||
if (quant_idx == 0u) {
|
||||
tree.nodes = joc_huff_code_coarse_coeff_sparse;
|
||||
tree.count = static_cast<int>(sizeof(joc_huff_code_coarse_coeff_sparse) /
|
||||
sizeof(joc_huff_code_coarse_coeff_sparse[0]));
|
||||
} else {
|
||||
tree.nodes = joc_huff_code_fine_coeff_sparse;
|
||||
tree.count = static_cast<int>(sizeof(joc_huff_code_fine_coeff_sparse) /
|
||||
sizeof(joc_huff_code_fine_coeff_sparse[0]));
|
||||
}
|
||||
break;
|
||||
case SymbolKind::Mtx:
|
||||
default:
|
||||
if (quant_idx == 0u) {
|
||||
tree.nodes = joc_huff_code_coarse_generic;
|
||||
tree.count = static_cast<int>(sizeof(joc_huff_code_coarse_generic) /
|
||||
sizeof(joc_huff_code_coarse_generic[0]));
|
||||
} else {
|
||||
tree.nodes = joc_huff_code_fine_generic;
|
||||
tree.count = static_cast<int>(sizeof(joc_huff_code_fine_generic) /
|
||||
sizeof(joc_huff_code_fine_generic[0]));
|
||||
}
|
||||
break;
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
// infinite loop (plan 40.4 BL-6).
|
||||
bool huff_decode(const Tree& tree, bits::BitReader& reader, std::int16_t* out_value) {
|
||||
int node = 0;
|
||||
int steps = 0;
|
||||
while (node >= 0) {
|
||||
if (node >= tree.count || steps > tree.count) {
|
||||
reader.fail(JOC_ERR_JOC_SYNTAX, "Huffman tree walk left the valid node range");
|
||||
return false;
|
||||
}
|
||||
++steps;
|
||||
const std::uint32_t bit = reader.read(1);
|
||||
if (reader.failed()) {
|
||||
return false;
|
||||
}
|
||||
node = tree.nodes[node][bit];
|
||||
}
|
||||
*out_value = static_cast<std::int16_t>(-node - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
Status syntax_fail(const std::string& message) {
|
||||
return Status::fail(JOC_ERR_JOC_SYNTAX, stage::kJoc, message);
|
||||
}
|
||||
|
||||
Status truncated_fail(const bits::BitReader& reader) {
|
||||
if (reader.error() == JOC_ERR_JOC_SYNTAX) {
|
||||
return syntax_fail(reader.error_message());
|
||||
}
|
||||
return Status::fail(JOC_ERR_BITSTREAM_TRUNCATED, stage::kJoc,
|
||||
std::string("JOC bitstream truncated: ") + reader.error_message());
|
||||
}
|
||||
|
||||
void reconstruct_dense(const ObjectSymbols& symbols, std::uint32_t dp, int n_channels, std::int64_t nquant,
|
||||
std::int64_t offset,
|
||||
std::int64_t q[JOC_MAX_DPOINTS][JOC_MAX_CORE_CHANNELS][JOC_MAX_PARAMETER_BANDS]) {
|
||||
for (int ch = 0; ch < n_channels; ++ch) {
|
||||
q[dp][ch][0] =
|
||||
floored_mod(offset + static_cast<std::int64_t>(symbols.mtx[dp][ch][0]), nquant);
|
||||
for (int pb = 1; pb < symbols.n_bands; ++pb) {
|
||||
q[dp][ch][pb] = floored_mod(
|
||||
q[dp][ch][pb - 1] + static_cast<std::int64_t>(symbols.mtx[dp][ch][pb]), nquant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// across parameter bands and is deliberately NOT reset when the active channel
|
||||
Status reconstruct_sparse(
|
||||
const ObjectSymbols& symbols, std::uint32_t dp, int n_channels, std::int64_t nquant, std::int64_t offset,
|
||||
std::int64_t q[JOC_MAX_DPOINTS][JOC_MAX_CORE_CHANNELS][JOC_MAX_PARAMETER_BANDS]) {
|
||||
if (n_channels != 5 && n_channels != 7) {
|
||||
return Status::fail(JOC_ERR_JOC_UNSUPPORTED_VARIANT, stage::kJoc,
|
||||
"sparse JOC requires 5 or 7 core channels, got " +
|
||||
std::to_string(n_channels));
|
||||
}
|
||||
const int initial_channel = symbols.idx[dp][0];
|
||||
if (initial_channel < 0 || initial_channel >= n_channels) {
|
||||
return syntax_fail("sparse JOC initial channel " + std::to_string(initial_channel) +
|
||||
" out of range for " + std::to_string(n_channels) + " channels");
|
||||
}
|
||||
// Non-active entries take nquant/2, which dequantizes to exactly zero.
|
||||
for (int ch = 0; ch < n_channels; ++ch) {
|
||||
for (int pb = 0; pb < symbols.n_bands; ++pb) {
|
||||
q[dp][ch][pb] = nquant / 2;
|
||||
}
|
||||
}
|
||||
int active = initial_channel;
|
||||
std::int64_t coefficient = offset;
|
||||
for (int pb = 0; pb < symbols.n_bands; ++pb) {
|
||||
if (pb != 0) {
|
||||
active = static_cast<int>(
|
||||
floored_mod(static_cast<std::int64_t>(active) + symbols.idx[dp][pb], n_channels));
|
||||
}
|
||||
coefficient = floored_mod(coefficient + static_cast<std::int64_t>(symbols.vec[dp][pb]), nquant);
|
||||
q[dp][active][pb] = coefficient;
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::int16_t num_channels_for_config(std::uint32_t dmx_config_idx) {
|
||||
for (const NumChannelsEntry& entry : kNumChannels) {
|
||||
if (entry.config == dmx_config_idx) {
|
||||
return entry.channels;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::int16_t num_bands_for_index(std::uint32_t num_bands_idx) {
|
||||
for (const NumBandsEntry& entry : kNumBands) {
|
||||
if (entry.index == num_bands_idx) {
|
||||
return entry.bands;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
Status parse_id14(const std::uint8_t* payload, std::size_t payload_size, joc_frame_params* out,
|
||||
FrameSymbols* symbols) {
|
||||
if (payload == nullptr || out == nullptr) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kJoc, "null payload or output");
|
||||
}
|
||||
std::memset(out, 0, sizeof(*out));
|
||||
out->struct_size = sizeof(joc_frame_params);
|
||||
out->struct_version = JOC_FRAME_PARAMS_VERSION;
|
||||
// capture is purely additive and never changes the parse result.
|
||||
FrameSymbols local_symbols{};
|
||||
FrameSymbols& capture = (symbols != nullptr) ? *symbols : local_symbols;
|
||||
std::memset(&capture, 0, sizeof(capture));
|
||||
|
||||
bits::BitReader reader(payload, payload_size);
|
||||
|
||||
out->dmx_config_idx = static_cast<std::uint8_t>(reader.read(3));
|
||||
out->num_objects_bits = static_cast<std::uint8_t>(reader.read(6));
|
||||
out->ext_config_idx = static_cast<std::uint8_t>(reader.read(3));
|
||||
|
||||
const std::uint32_t n_objects = static_cast<std::uint32_t>(out->num_objects_bits) + 1u;
|
||||
const std::int16_t n_channels = num_channels_for_config(out->dmx_config_idx);
|
||||
if (n_channels < 0) {
|
||||
return syntax_fail("unknown JOC downmix configuration " +
|
||||
std::to_string(out->dmx_config_idx));
|
||||
}
|
||||
out->n_channels = static_cast<std::uint8_t>(n_channels);
|
||||
if (n_objects > JOC_MAX_OBJECTS) {
|
||||
// The reference implementation has no check here and fails later inside
|
||||
// NumPy; the port reports it explicitly (plan 28.2).
|
||||
return Status::fail(JOC_ERR_JOC_UNSUPPORTED_VARIANT, stage::kJoc,
|
||||
"JOC frame declares " + std::to_string(n_objects) +
|
||||
" objects, the ABI supports at most " +
|
||||
std::to_string(static_cast<int>(JOC_MAX_OBJECTS)));
|
||||
}
|
||||
out->n_objects = static_cast<std::uint8_t>(n_objects);
|
||||
|
||||
out->clipgain_x_bits = static_cast<std::uint8_t>(reader.read(3));
|
||||
out->clipgain_y_bits = static_cast<std::uint8_t>(reader.read(5));
|
||||
out->seq_count = reader.read(10);
|
||||
// clipgain = 1 + (y/32) * 2^(x-4). The reference multiplies by an exact
|
||||
// exactly for the whole legal range.
|
||||
out->clipgain = 1.0 + static_cast<double>(out->clipgain_y_bits) / 32.0 *
|
||||
std::ldexp(1.0, static_cast<int>(out->clipgain_x_bits) - 4);
|
||||
|
||||
for (std::uint32_t obj = 0; obj < n_objects; ++obj) {
|
||||
joc_object_params& info = out->objects[obj];
|
||||
info.present = static_cast<std::uint8_t>(reader.read(1));
|
||||
if (info.present == 0u) {
|
||||
continue;
|
||||
}
|
||||
info.num_bands_idx = static_cast<std::uint8_t>(reader.read(3));
|
||||
const std::int16_t bands = num_bands_for_index(info.num_bands_idx);
|
||||
if (bands < 0) {
|
||||
return syntax_fail("unknown JOC num_bands index " +
|
||||
std::to_string(info.num_bands_idx));
|
||||
}
|
||||
info.n_bands = static_cast<std::uint8_t>(bands);
|
||||
info.sparse = static_cast<std::uint8_t>(reader.read(1));
|
||||
info.quant_idx = static_cast<std::uint8_t>(reader.read(1));
|
||||
info.slope_idx = static_cast<std::uint8_t>(reader.read(1));
|
||||
info.num_dpoints_bits = static_cast<std::uint8_t>(reader.read(1));
|
||||
info.n_dpoints = static_cast<std::uint8_t>(info.num_dpoints_bits + 1u);
|
||||
if (info.slope_idx == 1u) {
|
||||
for (std::uint32_t dp = 0; dp < info.n_dpoints; ++dp) {
|
||||
info.offset_ts[dp] = static_cast<std::uint8_t>(reader.read(5) + 1u);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reader.failed()) {
|
||||
return truncated_fail(reader);
|
||||
}
|
||||
|
||||
for (std::uint32_t obj = 0; obj < n_objects; ++obj) {
|
||||
const joc_object_params& info = out->objects[obj];
|
||||
if (info.present == 0u) {
|
||||
continue;
|
||||
}
|
||||
ObjectSymbols* symbol = &capture.objects[obj];
|
||||
symbol->present = 1;
|
||||
symbol->sparse = info.sparse;
|
||||
symbol->n_bands = info.n_bands;
|
||||
symbol->n_dpoints = info.n_dpoints;
|
||||
symbol->n_channels = out->n_channels;
|
||||
for (std::uint32_t dp = 0; dp < info.n_dpoints; ++dp) {
|
||||
if (info.sparse == 1u) {
|
||||
const Tree idx_tree = select_tree(info.quant_idx, SymbolKind::Idx, n_channels);
|
||||
const std::uint32_t first = reader.read(3);
|
||||
if (reader.failed()) {
|
||||
return truncated_fail(reader);
|
||||
}
|
||||
symbol->idx[dp][0] = static_cast<std::uint8_t>(first);
|
||||
for (int pb = 1; pb < info.n_bands; ++pb) {
|
||||
std::int16_t value = 0;
|
||||
if (!huff_decode(idx_tree, reader, &value)) {
|
||||
return truncated_fail(reader);
|
||||
}
|
||||
symbol->idx[dp][pb] = static_cast<std::uint8_t>(value);
|
||||
}
|
||||
const Tree vec_tree = select_tree(info.quant_idx, SymbolKind::Vec, n_channels);
|
||||
for (int pb = 0; pb < info.n_bands; ++pb) {
|
||||
std::int16_t value = 0;
|
||||
if (!huff_decode(vec_tree, reader, &value)) {
|
||||
return truncated_fail(reader);
|
||||
}
|
||||
symbol->vec[dp][pb] = value;
|
||||
}
|
||||
} else {
|
||||
const Tree mtx_tree = select_tree(info.quant_idx, SymbolKind::Mtx, n_channels);
|
||||
for (int ch = 0; ch < n_channels; ++ch) {
|
||||
for (int pb = 0; pb < info.n_bands; ++pb) {
|
||||
std::int16_t value = 0;
|
||||
if (!huff_decode(mtx_tree, reader, &value)) {
|
||||
return truncated_fail(reader);
|
||||
}
|
||||
symbol->mtx[dp][ch][pb] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out->data_end_bits = static_cast<std::uint32_t>(reader.position());
|
||||
out->trailing_bits = static_cast<std::uint32_t>(payload_size * 8u - reader.position());
|
||||
const std::size_t tail_offset = reader.position() / 8u;
|
||||
if (tail_offset < payload_size) {
|
||||
const std::size_t tail_bytes = std::min<std::size_t>(8u, payload_size - tail_offset);
|
||||
std::memcpy(out->tail_bytes, payload + tail_offset, tail_bytes);
|
||||
}
|
||||
|
||||
for (std::uint32_t obj = 0; obj < n_objects; ++obj) {
|
||||
const joc_object_params& info = out->objects[obj];
|
||||
if (info.present == 0u) {
|
||||
continue;
|
||||
}
|
||||
std::uint32_t mask_bit = 1u << obj;
|
||||
out->present_mask |= mask_bit;
|
||||
|
||||
const std::int64_t nquant = (info.quant_idx == 0u) ? 96 : 192;
|
||||
std::int64_t q[JOC_MAX_DPOINTS][JOC_MAX_CORE_CHANNELS][JOC_MAX_PARAMETER_BANDS] = {};
|
||||
ObjectSymbols* symbol = &capture.objects[obj];
|
||||
|
||||
for (std::uint32_t dp = 0; dp < info.n_dpoints; ++dp) {
|
||||
if (info.sparse == 1u) {
|
||||
const std::int64_t offset = (info.quant_idx == 0u) ? 50 : 100;
|
||||
const Status status =
|
||||
reconstruct_sparse(*symbol, dp, n_channels, nquant, offset, q);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
} else {
|
||||
const std::int64_t offset = (info.quant_idx == 0u) ? 48 : 96;
|
||||
reconstruct_dense(*symbol, dp, n_channels, nquant, offset, q);
|
||||
}
|
||||
}
|
||||
|
||||
// Operand order and types are kept identical to the reference so the
|
||||
// result is bit-exact, not merely close.
|
||||
const double nquant_half = static_cast<double>(nquant) / 2.0;
|
||||
const double denominator = 4096.0 * static_cast<double>(1 + static_cast<int>(info.quant_idx));
|
||||
for (std::uint32_t dp = 0; dp < info.n_dpoints; ++dp) {
|
||||
for (int ch = 0; ch < n_channels; ++ch) {
|
||||
for (int pb = 0; pb < info.n_bands; ++pb) {
|
||||
const double value = static_cast<double>(q[dp][ch][pb]) - nquant_half;
|
||||
out->objects[obj].dq[dp][ch][pb] = value * 820.0 / denominator;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (std::uint32_t dp = 0; dp < info.n_dpoints; ++dp) {
|
||||
for (int ch = 0; ch < n_channels; ++ch) {
|
||||
for (int pb = 0; pb < info.n_bands; ++pb) {
|
||||
symbol->q[dp][ch][pb] = q[dp][ch][pb];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
capture.n_objects = out->n_objects;
|
||||
capture.n_channels = out->n_channels;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status parse_eac3_frame(const std::uint8_t* frame, std::size_t frame_size, joc_frame_params* out,
|
||||
emdf::Container* container, FrameSymbols* symbols) {
|
||||
if (frame == nullptr || out == nullptr) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kJoc, "null frame or output");
|
||||
}
|
||||
emdf::Container local;
|
||||
const Status status = emdf::find_joc_emdf(frame, frame_size, &local);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (container != nullptr) {
|
||||
*container = local;
|
||||
}
|
||||
const emdf::Payload* payload = local.find(emdf::kIdJoc);
|
||||
if (payload == nullptr) {
|
||||
return Status::fail(JOC_ERR_EMDF_TRANSPORT, stage::kEmdf,
|
||||
"EMDF container has no ID14 (JOC) payload");
|
||||
}
|
||||
std::vector<std::uint8_t> bytes;
|
||||
const Status extract = emdf::extract_payload_bytes(frame, frame_size, *payload, &bytes);
|
||||
if (!extract.ok()) {
|
||||
return extract;
|
||||
}
|
||||
return parse_id14(bytes.data(), bytes.size(), out, symbols);
|
||||
}
|
||||
|
||||
Status check_id14_padding(const std::uint8_t* payload, std::size_t payload_size,
|
||||
std::uint32_t* out_trailing_bits) {
|
||||
joc_frame_params params;
|
||||
FrameSymbols symbols;
|
||||
const Status status = parse_id14(payload, payload_size, ¶ms, &symbols);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (out_trailing_bits != nullptr) {
|
||||
*out_trailing_bits = params.trailing_bits;
|
||||
}
|
||||
if (params.trailing_bits > 7u) {
|
||||
return Status::fail(JOC_ERR_BITSTREAM_PADDING, stage::kJoc,
|
||||
"more than 7 bits left after joc_data (" +
|
||||
std::to_string(params.trailing_bits) + ")");
|
||||
}
|
||||
for (std::size_t bit = params.data_end_bits; bit < payload_size * 8u; ++bit) {
|
||||
if (((payload[bit >> 3] >> (7u - (bit & 7u))) & 1u) != 0u) {
|
||||
return Status::fail(JOC_ERR_BITSTREAM_PADDING, stage::kJoc,
|
||||
"non-zero trailing padding bit at " + std::to_string(bit));
|
||||
}
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
} // namespace joc::joc
|
||||
@@ -0,0 +1,46 @@
|
||||
// Port of src/joc_decode.py.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "joc_core.h"
|
||||
#include "emdf/emdf_parser.h"
|
||||
#include "foundation/status.h"
|
||||
|
||||
namespace joc::joc {
|
||||
|
||||
struct ObjectSymbols {
|
||||
std::uint8_t present = 0;
|
||||
std::uint8_t sparse = 0;
|
||||
std::uint8_t n_bands = 0;
|
||||
std::uint8_t n_dpoints = 0;
|
||||
std::uint8_t n_channels = 0;
|
||||
std::int64_t q[JOC_MAX_DPOINTS][JOC_MAX_CORE_CHANNELS][JOC_MAX_PARAMETER_BANDS] = {};
|
||||
std::int16_t mtx[JOC_MAX_DPOINTS][JOC_MAX_CORE_CHANNELS][JOC_MAX_PARAMETER_BANDS] = {};
|
||||
std::uint8_t idx[JOC_MAX_DPOINTS][JOC_MAX_PARAMETER_BANDS] = {};
|
||||
std::int16_t vec[JOC_MAX_DPOINTS][JOC_MAX_PARAMETER_BANDS] = {};
|
||||
};
|
||||
|
||||
struct FrameSymbols {
|
||||
std::uint8_t n_objects = 0;
|
||||
std::uint8_t n_channels = 0;
|
||||
ObjectSymbols objects[JOC_MAX_OBJECTS];
|
||||
};
|
||||
|
||||
Status parse_id14(const std::uint8_t* payload, std::size_t payload_size, joc_frame_params* out,
|
||||
FrameSymbols* symbols);
|
||||
|
||||
Status parse_eac3_frame(const std::uint8_t* frame, std::size_t frame_size, joc_frame_params* out,
|
||||
emdf::Container* container, FrameSymbols* symbols);
|
||||
|
||||
Status check_id14_padding(const std::uint8_t* payload, std::size_t payload_size,
|
||||
std::uint32_t* out_trailing_bits);
|
||||
|
||||
std::int16_t num_channels_for_config(std::uint32_t dmx_config_idx);
|
||||
|
||||
std::int16_t num_bands_for_index(std::uint32_t num_bands_idx);
|
||||
|
||||
} // namespace joc::joc
|
||||
@@ -0,0 +1,735 @@
|
||||
#define EJOC_BUILD_DLL
|
||||
#include "eac3joc_core.h"
|
||||
#include "qmf_tables.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <barrier>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace ejoc {
|
||||
|
||||
struct Complex {
|
||||
double re;
|
||||
double im;
|
||||
};
|
||||
|
||||
inline Complex mul(const Complex a, const Complex b) noexcept {
|
||||
return {a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re};
|
||||
}
|
||||
|
||||
inline double clamp_unit(double value) noexcept {
|
||||
return value < -1.0 ? -1.0 : (value > 1.0 ? 1.0 : value);
|
||||
}
|
||||
|
||||
constexpr double kPi = 3.141592653589793238462643383279502884;
|
||||
constexpr int kLfeDelay = 1217;
|
||||
constexpr int kMaxBands = EJOC_MAX_PARAMETER_BANDS;
|
||||
|
||||
const uint8_t* parameter_band_map(const int bands) noexcept {
|
||||
using namespace tables;
|
||||
switch (bands) {
|
||||
case 1: return kPbMap1;
|
||||
case 3: return kPbMap3;
|
||||
case 5: return kPbMap5;
|
||||
case 7: return kPbMap7;
|
||||
case 9: return kPbMap9;
|
||||
case 12: return kPbMap12;
|
||||
case 15: return kPbMap15;
|
||||
case 23: return kPbMap23;
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
class Renderer final {
|
||||
public:
|
||||
Renderer() noexcept {
|
||||
initialize_tables();
|
||||
reset();
|
||||
}
|
||||
|
||||
~Renderer() noexcept {
|
||||
stop_workers();
|
||||
}
|
||||
|
||||
int reset() noexcept {
|
||||
std::memset(analysis_fifo_, 0, sizeof(analysis_fifo_));
|
||||
std::memset(analysis_delay_, 0, sizeof(analysis_delay_));
|
||||
std::memset(surround_delay_, 0, sizeof(surround_delay_));
|
||||
std::memset(surround_history_, 0, sizeof(surround_history_));
|
||||
std::memset(lfe_delay_, 0, sizeof(lfe_delay_));
|
||||
std::memset(matrix_previous_, 0, sizeof(matrix_previous_));
|
||||
std::memset(synthesis_state_, 0, sizeof(synthesis_state_));
|
||||
std::memset(x_, 0, sizeof(x_));
|
||||
std::memset(z_, 0, sizeof(z_));
|
||||
analysis_phase_ = 0.0625f;
|
||||
error_[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
int set_threads(uint32_t total_threads) noexcept {
|
||||
if (total_threads < 1) {
|
||||
total_threads = 1;
|
||||
}
|
||||
if (total_threads > EJOC_MAX_OBJECTS) {
|
||||
total_threads = EJOC_MAX_OBJECTS;
|
||||
}
|
||||
stop_workers();
|
||||
job_count_ = 0;
|
||||
next_job_.store(0, std::memory_order_relaxed);
|
||||
if (total_threads == 1) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
stop_.store(false, std::memory_order_relaxed);
|
||||
pool_ready_.store(false, std::memory_order_relaxed);
|
||||
work_barrier_ = std::make_unique<std::barrier<>>(static_cast<std::ptrdiff_t>(total_threads));
|
||||
workers_.reserve(total_threads - 1);
|
||||
for (uint32_t index = 1; index < total_threads; ++index) {
|
||||
workers_.emplace_back([this]() noexcept { worker_loop(); });
|
||||
}
|
||||
pool_ready_.store(true, std::memory_order_release);
|
||||
// Startup rendezvous: ensure every worker has entered the two-phase
|
||||
// barrier loop before set_threads returns, so an immediate destroy
|
||||
// cannot race a worker that exits before reaching the barrier.
|
||||
work_barrier_->arrive_and_wait();
|
||||
work_barrier_->arrive_and_wait();
|
||||
} catch (...) {
|
||||
stop_.store(true, std::memory_order_release);
|
||||
pool_ready_.store(true, std::memory_order_release);
|
||||
for (std::thread& worker : workers_) {
|
||||
if (worker.joinable()) {
|
||||
worker.join();
|
||||
}
|
||||
}
|
||||
workers_.clear();
|
||||
work_barrier_.reset();
|
||||
stop_.store(false, std::memory_order_relaxed);
|
||||
return fail("failed to create native worker threads");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t thread_count() const noexcept {
|
||||
return static_cast<uint32_t>(workers_.size() + 1);
|
||||
}
|
||||
|
||||
const char* error() const noexcept {
|
||||
return error_[0] ? error_ : "";
|
||||
}
|
||||
|
||||
int process(
|
||||
const float* bed5,
|
||||
const float* lfe,
|
||||
const uint32_t object_mask,
|
||||
const uint8_t* n_bands,
|
||||
const uint8_t* n_dpoints,
|
||||
const uint8_t* slope_idx,
|
||||
const uint8_t* offset_ts,
|
||||
const double* dq,
|
||||
const double clipgain,
|
||||
const float phase_new,
|
||||
const float output_scale,
|
||||
float* output16) noexcept {
|
||||
|
||||
error_[0] = '\0';
|
||||
if (!bed5 || !n_bands || !n_dpoints || !slope_idx || !offset_ts || !dq || !output16) {
|
||||
return fail("null pointer passed to ejoc_renderer_process");
|
||||
}
|
||||
if (object_mask & ~((1u << EJOC_MAX_OBJECTS) - 1u)) {
|
||||
return fail("object_mask contains an object index above 14");
|
||||
}
|
||||
if (!std::isfinite(clipgain) || !std::isfinite(phase_new) || !std::isfinite(output_scale)) {
|
||||
return fail("clipgain, phase_new, and output_scale must be finite");
|
||||
}
|
||||
for (int object = 0; object < EJOC_MAX_OBJECTS; ++object) {
|
||||
if ((object_mask & (1u << object)) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!parameter_band_map(n_bands[object])) {
|
||||
return fail("unsupported parameter-band count");
|
||||
}
|
||||
if (n_dpoints[object] < 1 || n_dpoints[object] > 2) {
|
||||
return fail("n_dpoints must be 1 or 2");
|
||||
}
|
||||
if (slope_idx[object] > 1) {
|
||||
return fail("slope_idx must be 0 or 1");
|
||||
}
|
||||
}
|
||||
|
||||
std::memset(output16, 0, sizeof(float) * EJOC_OUTPUT_CHANNELS * EJOC_FRAME_SAMPLES);
|
||||
analysis(bed5, phase_new);
|
||||
render_lfe(lfe, output_scale, output16);
|
||||
process_objects(object_mask, n_bands, n_dpoints, slope_idx, offset_ts,
|
||||
dq, clipgain, output_scale, output16);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
void initialize_tables() noexcept {
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
int value = i;
|
||||
int reversed = 0;
|
||||
for (int bit = 0; bit < 6; ++bit) {
|
||||
reversed = (reversed << 1) | (value & 1);
|
||||
value >>= 1;
|
||||
}
|
||||
bit_reverse_[i] = static_cast<uint8_t>(reversed);
|
||||
const double theta = kPi * static_cast<double>(i) / 128.0;
|
||||
rotation_sin_[i] = 0.5 * std::sin(theta);
|
||||
rotation_cos_[i] = 0.5 * std::cos(theta);
|
||||
}
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
const double angle = -2.0 * kPi * static_cast<double>(i) / 64.0;
|
||||
fft_twiddle_[i] = {std::cos(angle), std::sin(angle)};
|
||||
}
|
||||
}
|
||||
|
||||
int fail(const char* message) noexcept {
|
||||
std::snprintf(error_, sizeof(error_), "%s", message);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void fft64(Complex* values) const noexcept {
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
const int j = bit_reverse_[i];
|
||||
if (j > i) {
|
||||
const Complex temp = values[i];
|
||||
values[i] = values[j];
|
||||
values[j] = temp;
|
||||
}
|
||||
}
|
||||
for (int length = 2; length <= 64; length <<= 1) {
|
||||
const int half = length >> 1;
|
||||
const int twiddle_step = 64 / length;
|
||||
for (int base = 0; base < 64; base += length) {
|
||||
for (int j = 0; j < half; ++j) {
|
||||
const Complex even = values[base + j];
|
||||
const Complex odd = mul(values[base + j + half], fft_twiddle_[j * twiddle_step]);
|
||||
values[base + j] = {even.re + odd.re, even.im + odd.im};
|
||||
values[base + j + half] = {even.re - odd.re, even.im - odd.im};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void analysis_slot(const int channel, const float* ring, const int timeslot) noexcept {
|
||||
double v36[64];
|
||||
double v40[64];
|
||||
Complex frequency[64];
|
||||
|
||||
for (int sample = 0; sample < 64; ++sample) {
|
||||
v36[sample] =
|
||||
analysis_fifo_[channel][0][sample] * tables::kAnalysisWindow[8 * 64 + sample] +
|
||||
analysis_fifo_[channel][2][sample] * tables::kAnalysisWindow[6 * 64 + sample] +
|
||||
analysis_fifo_[channel][4][sample] * tables::kAnalysisWindow[4 * 64 + sample] +
|
||||
analysis_fifo_[channel][6][sample] * tables::kAnalysisWindow[2 * 64 + sample] +
|
||||
analysis_fifo_[channel][8][sample] * tables::kAnalysisWindow[0 * 64 + sample];
|
||||
v40[sample] =
|
||||
analysis_fifo_[channel][1][sample] * tables::kAnalysisWindow[7 * 64 + sample] +
|
||||
analysis_fifo_[channel][3][sample] * tables::kAnalysisWindow[5 * 64 + sample] +
|
||||
analysis_fifo_[channel][5][sample] * tables::kAnalysisWindow[3 * 64 + sample] +
|
||||
analysis_fifo_[channel][7][sample] * tables::kAnalysisWindow[1 * 64 + sample] +
|
||||
static_cast<double>(ring[sample]) * tables::kAnalysisWindow[9 * 64 + sample];
|
||||
}
|
||||
|
||||
for (int k = 0; k < 64; ++k) {
|
||||
const int source = 63 - k;
|
||||
const double re = v40[source];
|
||||
const double im = v36[source];
|
||||
const double a = rotation_sin_[k];
|
||||
const double b = rotation_cos_[k];
|
||||
frequency[k] = {im * a - re * b, im * b + re * a};
|
||||
}
|
||||
fft64(frequency);
|
||||
constexpr double scale = 1.0 / 64.0;
|
||||
for (int k = 0; k < 32; ++k) {
|
||||
x_[channel][2 * k][timeslot] = {frequency[k].re * scale, -frequency[k].im * scale};
|
||||
x_[channel][2 * k + 1][timeslot] = {
|
||||
frequency[63 - k].re * scale,
|
||||
frequency[63 - k].im * scale};
|
||||
}
|
||||
|
||||
for (int history = 8; history > 0; --history) {
|
||||
std::memcpy(analysis_fifo_[channel][history], analysis_fifo_[channel][history - 1],
|
||||
sizeof(analysis_fifo_[channel][history]));
|
||||
}
|
||||
for (int sample = 0; sample < 64; ++sample) {
|
||||
analysis_fifo_[channel][0][sample] = static_cast<double>(ring[sample]);
|
||||
}
|
||||
}
|
||||
|
||||
void surround_post() noexcept {
|
||||
static constexpr double kDcA[21] = {
|
||||
-.0006242550443857908, -.0019234686624258757, -.0042654648423194885,
|
||||
-.008168308064341545, -.014327201060950756, -.023759860545396805,
|
||||
-.03757232800126076, -.05577569454908371, -.07568276673555374,
|
||||
-.09172472357749939, -.5979374051094055, -.09172472357749939,
|
||||
-.07568276673555374, -.05577569454908371, -.03757232800126076,
|
||||
-.023759860545396805, -.014327201060950756, -.008168308064341545,
|
||||
-.0042654648423194885, -.0019234686624258757, -.0006242550443857908,
|
||||
};
|
||||
static constexpr double kDcB[21] = {
|
||||
.0013996040215715766, .003839150769636035, .007512642536312342,
|
||||
.012419373728334904, .018367428332567215, .0249701626598835,
|
||||
.03167900815606117, .03785000368952751, .04283412545919418,
|
||||
.04607561603188515, .047200120985507965, .04607561603188515,
|
||||
.04283412545919418, .03785000368952751, .03167900815606117,
|
||||
.0249701626598835, .018367428332567215, .012419373728334904,
|
||||
.007512642536312342, .003839150769636035, .0013996040215715766,
|
||||
};
|
||||
|
||||
for (int surround = 0; surround < 2; ++surround) {
|
||||
const int channel = surround + 3;
|
||||
for (int group = 0; group < 24; group += 4) {
|
||||
Complex current[4][64];
|
||||
Complex dc_buffer[24];
|
||||
for (int slot = 0; slot < 4; ++slot) {
|
||||
for (int band = 0; band < 64; ++band) {
|
||||
current[slot][band] = x_[channel][band][group + slot];
|
||||
const Complex delayed = surround_delay_[surround][slot][band];
|
||||
x_[channel][band][group + slot] = {delayed.im, -delayed.re};
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
dc_buffer[i] = surround_history_[surround][i];
|
||||
}
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
dc_buffer[20 + i] = current[i][0];
|
||||
}
|
||||
for (int slot = 0; slot < 4; ++slot) {
|
||||
Complex sum{0.0, 0.0};
|
||||
for (int tap = 0; tap < 21; ++tap) {
|
||||
const Complex sample = dc_buffer[slot + tap];
|
||||
const double cr = kDcB[tap];
|
||||
const double ci = kDcA[tap];
|
||||
sum.re += sample.re * cr - sample.im * ci;
|
||||
sum.im += sample.re * ci + sample.im * cr;
|
||||
}
|
||||
x_[channel][0][group + slot] = {2.0 * sum.re, 2.0 * sum.im};
|
||||
}
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
surround_history_[surround][i] = dc_buffer[i + 4];
|
||||
}
|
||||
|
||||
std::memmove(&surround_delay_[surround][0][0],
|
||||
&surround_delay_[surround][4][0],
|
||||
sizeof(Complex) * 6 * 64);
|
||||
for (int slot = 0; slot < 4; ++slot) {
|
||||
std::memcpy(surround_delay_[surround][6 + slot], current[slot],
|
||||
sizeof(Complex) * 64);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void analysis_channel(const int channel) noexcept {
|
||||
const float* bed5 = job_bed5_;
|
||||
const float phase_old = job_phase_old_;
|
||||
const float phase_new = job_phase_new_;
|
||||
const bool ramp_phase = phase_old != phase_new;
|
||||
const float phase_step = static_cast<float>((phase_new - phase_old) / 256.0f);
|
||||
float scaled[64];
|
||||
float delayed[64];
|
||||
|
||||
for (int timeslot = 0; timeslot < 24; ++timeslot) {
|
||||
for (int sample = 0; sample < 64; ++sample) {
|
||||
const int frame_sample = timeslot * 64 + sample;
|
||||
float gain = phase_new;
|
||||
if (ramp_phase && frame_sample < 256) {
|
||||
const float product = static_cast<float>(static_cast<float>(frame_sample) * phase_step);
|
||||
gain = static_cast<float>(phase_old + product);
|
||||
}
|
||||
scaled[sample] = static_cast<float>(bed5[channel * 1536 + frame_sample] * gain);
|
||||
}
|
||||
const float* analysis_input = scaled;
|
||||
if (channel < 3) {
|
||||
std::memcpy(delayed, analysis_delay_[channel][0], sizeof(delayed));
|
||||
std::memmove(&analysis_delay_[channel][0][0],
|
||||
&analysis_delay_[channel][1][0],
|
||||
sizeof(float) * 9 * 64);
|
||||
std::memcpy(analysis_delay_[channel][9], scaled, sizeof(scaled));
|
||||
analysis_input = delayed;
|
||||
}
|
||||
analysis_slot(channel, analysis_input, timeslot);
|
||||
}
|
||||
}
|
||||
|
||||
void analysis(const float* bed5, const float phase_new) noexcept {
|
||||
job_bed5_ = bed5;
|
||||
job_phase_old_ = analysis_phase_;
|
||||
job_phase_new_ = phase_new;
|
||||
job_kind_ = JobKind::Analysis;
|
||||
job_count_ = 5;
|
||||
for (int channel = 0; channel < 5; ++channel) {
|
||||
job_objects_[channel] = channel;
|
||||
}
|
||||
dispatch_jobs();
|
||||
analysis_phase_ = phase_new;
|
||||
surround_post();
|
||||
}
|
||||
|
||||
static std::size_t dq_index(const int object, const int point, const int channel, const int band) noexcept {
|
||||
return static_cast<std::size_t>((((object * 2 + point) * 5 + channel) * kMaxBands) + band);
|
||||
}
|
||||
|
||||
void matrix_object(
|
||||
const int object,
|
||||
const uint8_t* n_bands,
|
||||
const uint8_t* n_dpoints,
|
||||
const uint8_t* slope_idx,
|
||||
const uint8_t* offset_ts,
|
||||
const double* dq) noexcept {
|
||||
|
||||
std::memset(z_[object], 0, sizeof(z_[object]));
|
||||
const int bands = n_bands[object];
|
||||
const int points = n_dpoints[object];
|
||||
const int slope = slope_idx[object];
|
||||
const uint8_t* pb_map = parameter_band_map(bands);
|
||||
|
||||
for (int channel = 0; channel < 5; ++channel) {
|
||||
for (int subband = 0; subband < 64; ++subband) {
|
||||
const int parameter_band = pb_map[subband];
|
||||
const double previous = matrix_previous_[object][channel][subband];
|
||||
const double target0 = dq[dq_index(object, 0, channel, parameter_band)];
|
||||
const double target1 = points == 2
|
||||
? dq[dq_index(object, 1, channel, parameter_band)]
|
||||
: target0;
|
||||
double last = previous;
|
||||
|
||||
for (int timeslot = 0; timeslot < 24; ++timeslot) {
|
||||
double coefficient;
|
||||
if (slope == 0) {
|
||||
if (points == 1) {
|
||||
const double alpha = static_cast<double>(timeslot + 1) / 24.0;
|
||||
coefficient = previous * (1.0 - alpha) + target0 * alpha;
|
||||
} else if (timeslot < 12) {
|
||||
const double alpha = static_cast<double>(timeslot + 1) / 12.0;
|
||||
coefficient = previous * (1.0 - alpha) + target0 * alpha;
|
||||
} else {
|
||||
const double alpha = static_cast<double>(timeslot - 11) / 12.0;
|
||||
coefficient = target0 * (1.0 - alpha) + target1 * alpha;
|
||||
}
|
||||
} else if (points == 1) {
|
||||
coefficient = timeslot < offset_ts[object * 2] ? previous : target0;
|
||||
} else {
|
||||
coefficient = timeslot < offset_ts[object * 2] ? previous : target0;
|
||||
if (timeslot >= offset_ts[object * 2 + 1]) {
|
||||
coefficient = target1;
|
||||
}
|
||||
}
|
||||
z_[object][subband][timeslot].re += x_[channel][subband][timeslot].re * coefficient;
|
||||
z_[object][subband][timeslot].im += x_[channel][subband][timeslot].im * coefficient;
|
||||
last = coefficient;
|
||||
}
|
||||
matrix_previous_[object][channel][subband] = last;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void qmf5_step(double* state, const double* rotated, double* output) noexcept {
|
||||
for (int block = 0; block < 16; ++block) {
|
||||
for (int lane = 0; lane < 4; ++lane) {
|
||||
const int sample = block * 4 + lane;
|
||||
const int state_base = block * 36 + lane;
|
||||
const double even = rotated[block * 8 + lane * 2];
|
||||
const double odd = rotated[block * 8 + lane * 2 + 1];
|
||||
output[sample] = 2.0 * (
|
||||
tables::kQmf5Window[sample] * even + state[state_base]);
|
||||
|
||||
state[state_base] = tables::kQmf5Window[1 * 64 + sample] * odd + state[state_base + 4];
|
||||
for (int slot = 0; slot < 7; ++slot) {
|
||||
const double alternating = (slot & 1) == 0 ? even : odd;
|
||||
state[state_base + (slot + 1) * 4] =
|
||||
tables::kQmf5Window[(slot + 2) * 64 + sample] * alternating +
|
||||
state[state_base + (slot + 2) * 4];
|
||||
}
|
||||
state[state_base + 8 * 4] = tables::kQmf5Window[9 * 64 + sample] * odd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void synthesis_object(
|
||||
const int object,
|
||||
const double clipgain,
|
||||
const float output_scale,
|
||||
float* output16) noexcept {
|
||||
|
||||
Complex frequency[64];
|
||||
double rotated[128];
|
||||
double pcm64[64];
|
||||
double* state = synthesis_state_[object];
|
||||
float* destination = output16 + (object + 1) * 1536;
|
||||
for (int timeslot = 0; timeslot < 24; ++timeslot) {
|
||||
for (int k = 0; k < 32; ++k) {
|
||||
const Complex even = z_[object][2 * k][timeslot];
|
||||
const Complex odd = z_[object][2 * k + 1][timeslot];
|
||||
frequency[k] = {even.re, -even.im};
|
||||
frequency[63 - k] = {odd.re, odd.im};
|
||||
}
|
||||
fft64(frequency);
|
||||
for (int k = 0; k < 64; ++k) {
|
||||
const double re = frequency[k].re;
|
||||
const double im = frequency[k].im;
|
||||
const double sin_component = rotation_sin_[k];
|
||||
const double cos_component = rotation_cos_[k];
|
||||
rotated[2 * k] = 2.0 * (re * cos_component + im * sin_component);
|
||||
rotated[2 * k + 1] = 2.0 * (im * cos_component - re * sin_component);
|
||||
}
|
||||
qmf5_step(state, rotated, pcm64);
|
||||
for (int sample = 0; sample < 64; ++sample) {
|
||||
const double clipped = clamp_unit(16.0 * pcm64[sample]);
|
||||
const float value = static_cast<float>(clipped * clipgain);
|
||||
destination[timeslot * 64 + sample] = static_cast<float>(value * output_scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void process_one_object(const int object) noexcept {
|
||||
matrix_object(object, job_n_bands_, job_n_dpoints_, job_slope_idx_,
|
||||
job_offset_ts_, job_dq_);
|
||||
synthesis_object(object, job_clipgain_, job_output_scale_, job_output16_);
|
||||
}
|
||||
|
||||
void execute_job_loop() noexcept {
|
||||
while (true) {
|
||||
const int index = next_job_.fetch_add(1, std::memory_order_relaxed);
|
||||
if (index >= job_count_) {
|
||||
break;
|
||||
}
|
||||
const int item = job_objects_[index];
|
||||
if (job_kind_ == JobKind::Analysis) {
|
||||
analysis_channel(item);
|
||||
} else {
|
||||
process_one_object(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void dispatch_jobs() noexcept {
|
||||
if (workers_.empty() || job_count_ == 1) {
|
||||
next_job_.store(0, std::memory_order_relaxed);
|
||||
execute_job_loop();
|
||||
return;
|
||||
}
|
||||
next_job_.store(0, std::memory_order_relaxed);
|
||||
work_barrier_->arrive_and_wait();
|
||||
execute_job_loop();
|
||||
work_barrier_->arrive_and_wait();
|
||||
}
|
||||
|
||||
void worker_loop() noexcept {
|
||||
while (!pool_ready_.load(std::memory_order_acquire)) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
if (stop_.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
while (true) {
|
||||
work_barrier_->arrive_and_wait();
|
||||
if (stop_.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
execute_job_loop();
|
||||
work_barrier_->arrive_and_wait();
|
||||
}
|
||||
}
|
||||
|
||||
void stop_workers() noexcept {
|
||||
if (workers_.empty()) {
|
||||
work_barrier_.reset();
|
||||
stop_.store(false, std::memory_order_relaxed);
|
||||
pool_ready_.store(false, std::memory_order_relaxed);
|
||||
return;
|
||||
}
|
||||
stop_.store(true, std::memory_order_release);
|
||||
work_barrier_->arrive_and_wait();
|
||||
for (std::thread& worker : workers_) {
|
||||
if (worker.joinable()) {
|
||||
worker.join();
|
||||
}
|
||||
}
|
||||
workers_.clear();
|
||||
work_barrier_.reset();
|
||||
stop_.store(false, std::memory_order_relaxed);
|
||||
pool_ready_.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void process_objects(
|
||||
const uint32_t object_mask,
|
||||
const uint8_t* n_bands,
|
||||
const uint8_t* n_dpoints,
|
||||
const uint8_t* slope_idx,
|
||||
const uint8_t* offset_ts,
|
||||
const double* dq,
|
||||
const double clipgain,
|
||||
const float output_scale,
|
||||
float* output16) noexcept {
|
||||
|
||||
int count = 0;
|
||||
for (int object = 0; object < EJOC_MAX_OBJECTS; ++object) {
|
||||
if (object_mask & (1u << object)) {
|
||||
job_objects_[count++] = object;
|
||||
}
|
||||
}
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
job_kind_ = JobKind::Objects;
|
||||
job_count_ = count;
|
||||
job_n_bands_ = n_bands;
|
||||
job_n_dpoints_ = n_dpoints;
|
||||
job_slope_idx_ = slope_idx;
|
||||
job_offset_ts_ = offset_ts;
|
||||
job_dq_ = dq;
|
||||
job_clipgain_ = clipgain;
|
||||
job_output_scale_ = output_scale;
|
||||
job_output16_ = output16;
|
||||
|
||||
dispatch_jobs();
|
||||
}
|
||||
|
||||
void render_lfe(const float* lfe, const float output_scale, float* output16) noexcept {
|
||||
if (!lfe) {
|
||||
return;
|
||||
}
|
||||
for (int sample = 0; sample < kLfeDelay; ++sample) {
|
||||
const float value = static_cast<float>(clamp_unit(lfe_delay_[sample]));
|
||||
output16[sample] = static_cast<float>(value * output_scale);
|
||||
}
|
||||
for (int sample = kLfeDelay; sample < 1536; ++sample) {
|
||||
const float value = static_cast<float>(clamp_unit(static_cast<double>(lfe[sample - kLfeDelay])));
|
||||
output16[sample] = static_cast<float>(value * output_scale);
|
||||
}
|
||||
for (int sample = 0; sample < kLfeDelay; ++sample) {
|
||||
lfe_delay_[sample] = static_cast<double>(lfe[sample + (1536 - kLfeDelay)]);
|
||||
}
|
||||
}
|
||||
|
||||
enum class JobKind : uint8_t { Analysis, Objects };
|
||||
|
||||
std::vector<std::thread> workers_;
|
||||
std::unique_ptr<std::barrier<>> work_barrier_;
|
||||
std::atomic<bool> stop_{false};
|
||||
std::atomic<bool> pool_ready_{false};
|
||||
std::atomic<int> next_job_{0};
|
||||
int job_objects_[EJOC_MAX_OBJECTS]{};
|
||||
int job_count_ = 0;
|
||||
JobKind job_kind_ = JobKind::Objects;
|
||||
const float* job_bed5_ = nullptr;
|
||||
float job_phase_old_ = 0.0625f;
|
||||
float job_phase_new_ = 0.0625f;
|
||||
const uint8_t* job_n_bands_ = nullptr;
|
||||
const uint8_t* job_n_dpoints_ = nullptr;
|
||||
const uint8_t* job_slope_idx_ = nullptr;
|
||||
const uint8_t* job_offset_ts_ = nullptr;
|
||||
const double* job_dq_ = nullptr;
|
||||
double job_clipgain_ = 1.0;
|
||||
float job_output_scale_ = 1.0f;
|
||||
float* job_output16_ = nullptr;
|
||||
|
||||
alignas(64) double analysis_fifo_[5][9][64];
|
||||
alignas(64) float analysis_delay_[3][10][64];
|
||||
float analysis_phase_;
|
||||
alignas(64) Complex surround_delay_[2][10][64];
|
||||
alignas(64) Complex surround_history_[2][20];
|
||||
alignas(64) double lfe_delay_[kLfeDelay];
|
||||
alignas(64) double matrix_previous_[15][5][64];
|
||||
alignas(64) double synthesis_state_[15][640];
|
||||
alignas(64) Complex x_[5][64][24];
|
||||
alignas(64) Complex z_[15][64][24];
|
||||
|
||||
uint8_t bit_reverse_[64];
|
||||
Complex fft_twiddle_[32];
|
||||
double rotation_sin_[64];
|
||||
double rotation_cos_[64];
|
||||
char error_[256];
|
||||
};
|
||||
|
||||
} // namespace ejoc
|
||||
|
||||
extern "C" {
|
||||
|
||||
uint32_t EJOC_CALL ejoc_abi_version(void) {
|
||||
return EJOC_ABI_VERSION;
|
||||
}
|
||||
|
||||
const char* EJOC_CALL ejoc_build_info(void) {
|
||||
#if defined(_MSC_VER)
|
||||
return "eac3joc-core abi=1 compiler=MSVC fft=fixed64 speaker=double binaural=double crt=static-by-build";
|
||||
#elif defined(__clang__)
|
||||
return "eac3joc-core abi=1 compiler=Clang fft=fixed64 speaker=double binaural=double";
|
||||
#elif defined(__GNUC__)
|
||||
return "eac3joc-core abi=1 compiler=GCC fft=fixed64 speaker=double binaural=double";
|
||||
#else
|
||||
return "eac3joc-core abi=1 compiler=unknown fft=fixed64 speaker=double binaural=double";
|
||||
#endif
|
||||
}
|
||||
|
||||
ejoc_renderer_handle EJOC_CALL ejoc_renderer_create(void) {
|
||||
return new (std::nothrow) ejoc::Renderer();
|
||||
}
|
||||
|
||||
void EJOC_CALL ejoc_renderer_destroy(ejoc_renderer_handle handle) {
|
||||
delete static_cast<ejoc::Renderer*>(handle);
|
||||
}
|
||||
|
||||
int EJOC_CALL ejoc_renderer_reset(ejoc_renderer_handle handle) {
|
||||
if (!handle) {
|
||||
return -1;
|
||||
}
|
||||
return static_cast<ejoc::Renderer*>(handle)->reset();
|
||||
}
|
||||
|
||||
int EJOC_CALL ejoc_renderer_set_threads(ejoc_renderer_handle handle, uint32_t total_threads) {
|
||||
if (!handle) {
|
||||
return -1;
|
||||
}
|
||||
return static_cast<ejoc::Renderer*>(handle)->set_threads(total_threads);
|
||||
}
|
||||
|
||||
uint32_t EJOC_CALL ejoc_renderer_thread_count(ejoc_renderer_handle handle) {
|
||||
if (!handle) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<ejoc::Renderer*>(handle)->thread_count();
|
||||
}
|
||||
|
||||
const char* EJOC_CALL ejoc_renderer_last_error(ejoc_renderer_handle handle) {
|
||||
if (!handle) {
|
||||
return "null renderer handle";
|
||||
}
|
||||
return static_cast<ejoc::Renderer*>(handle)->error();
|
||||
}
|
||||
|
||||
int EJOC_CALL ejoc_renderer_process(
|
||||
ejoc_renderer_handle handle,
|
||||
const float* bed5_planar,
|
||||
const float* lfe,
|
||||
const uint32_t object_mask,
|
||||
const uint8_t* n_bands,
|
||||
const uint8_t* n_dpoints,
|
||||
const uint8_t* slope_idx,
|
||||
const uint8_t* offset_ts,
|
||||
const double* dq,
|
||||
const double clipgain,
|
||||
const float phase_new,
|
||||
const float output_scale,
|
||||
float* output16_planar) {
|
||||
if (!handle) {
|
||||
return -1;
|
||||
}
|
||||
return static_cast<ejoc::Renderer*>(handle)->process(
|
||||
bed5_planar, lfe, object_mask, n_bands, n_dpoints, slope_idx,
|
||||
offset_ts, dq, clipgain, phase_new, output_scale, output16_planar);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,71 @@
|
||||
#include "joc_core/objects16.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace joc::joc {
|
||||
|
||||
Status rebuild_objects16(ejoc_renderer_handle handle, const joc_frame_params& params,
|
||||
const float* bed5_planar, const float* lfe, float gain,
|
||||
std::vector<float>* out16, std::string* error) {
|
||||
if (handle == nullptr || bed5_planar == nullptr || out16 == nullptr) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kDsp, "null argument");
|
||||
}
|
||||
if (params.n_channels != JOC_CORE_CHANNELS) {
|
||||
if (error != nullptr) {
|
||||
*error = "the reused JOC kernel requires 5 core channels, frame declares " +
|
||||
std::to_string(static_cast<unsigned>(params.n_channels));
|
||||
}
|
||||
return Status::fail(JOC_ERR_JOC_UNSUPPORTED_VARIANT, stage::kDsp, *error);
|
||||
}
|
||||
|
||||
std::vector<double> dq(static_cast<std::size_t>(JOC_MAX_OBJECTS) * JOC_MAX_DPOINTS *
|
||||
JOC_CORE_CHANNELS * JOC_MAX_PARAMETER_BANDS,
|
||||
0.0);
|
||||
std::uint8_t n_bands[JOC_MAX_OBJECTS] = {};
|
||||
std::uint8_t n_dpoints[JOC_MAX_OBJECTS] = {};
|
||||
std::uint8_t slope_idx[JOC_MAX_OBJECTS] = {};
|
||||
std::uint8_t offset_ts[JOC_MAX_OBJECTS * JOC_MAX_DPOINTS] = {};
|
||||
for (unsigned obj = 0; obj < JOC_MAX_OBJECTS; ++obj) {
|
||||
const joc_object_params& object = params.objects[obj];
|
||||
if (object.present == 0) {
|
||||
continue;
|
||||
}
|
||||
n_bands[obj] = object.n_bands;
|
||||
n_dpoints[obj] = object.n_dpoints;
|
||||
slope_idx[obj] = object.slope_idx;
|
||||
for (unsigned dp = 0; dp < JOC_MAX_DPOINTS; ++dp) {
|
||||
offset_ts[obj * JOC_MAX_DPOINTS + dp] = object.offset_ts[dp];
|
||||
}
|
||||
for (unsigned dp = 0; dp < object.n_dpoints; ++dp) {
|
||||
for (unsigned ch = 0; ch < JOC_CORE_CHANNELS; ++ch) {
|
||||
for (unsigned pb = 0; pb < object.n_bands; ++pb) {
|
||||
const std::size_t index =
|
||||
((static_cast<std::size_t>(obj) * JOC_MAX_DPOINTS + dp) * JOC_CORE_CHANNELS +
|
||||
ch) *
|
||||
JOC_MAX_PARAMETER_BANDS +
|
||||
pb;
|
||||
dq[index] = object.dq[dp][ch][pb];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out16->assign(static_cast<std::size_t>(JOC_OUTPUT_CHANNELS) * JOC_FRAME_SAMPLES, 0.0f);
|
||||
const int result = ejoc_renderer_process(
|
||||
handle, bed5_planar, lfe, params.present_mask, n_bands, n_dpoints, slope_idx, offset_ts,
|
||||
dq.data(), params.clipgain, 0.0625f, gain, out16->data());
|
||||
if (result != 0) {
|
||||
const char* detail = ejoc_renderer_last_error(handle);
|
||||
const std::string message =
|
||||
"ejoc_renderer_process failed (" + std::to_string(result) + "): " +
|
||||
(detail != nullptr ? detail : "unknown");
|
||||
if (error != nullptr) {
|
||||
*error = message;
|
||||
}
|
||||
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kDsp, message);
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
} // namespace joc::joc
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "eac3joc_core.h"
|
||||
#include "foundation/status.h"
|
||||
#include "joc_core.h"
|
||||
|
||||
namespace joc::joc {
|
||||
|
||||
// [16][1536] planar float32. The kernel requires exactly 5 core channels.
|
||||
Status rebuild_objects16(ejoc_renderer_handle handle, const joc_frame_params& params,
|
||||
const float* bed5_planar, const float* lfe, float gain,
|
||||
std::vector<float>* out16, std::string* error);
|
||||
|
||||
} // namespace joc::joc
|
||||
@@ -0,0 +1,388 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Generated table data; do not hand-edit.
|
||||
namespace ejoc::tables {
|
||||
inline constexpr double kAnalysisWindow[640] = {
|
||||
0.00019903187057934701, 0.00024947625934146345, 0.00030217695166356862, 0.00035484600812196732,
|
||||
0.0004058915947098285, 0.00045464080176316202, 0.00050126801943406463, 0.00054649583762511611,
|
||||
0.00059120741207152605, 0.0006361178238876164, 0.00068160606315359473, 0.00072772568091750145,
|
||||
0.00077434181002900004, 0.00082129903603345156, 0.00086853635730221868, 0.00091610715026035905,
|
||||
0.00096411682898178697, 0.0010126305278390646, 0.0010616052895784378, 0.0011108826147392392,
|
||||
0.0011602368904277682, 0.0012094489065930247, 0.0012583627831190825, 0.0013069023843854666,
|
||||
0.0013550462899729609, 0.0014027846045792103, 0.0014500867109745741, 0.0014968989416956902,
|
||||
0.0015431707724928856, 0.0015888890484347939, 0.0016340982401743531, 0.0016788924112915993,
|
||||
0.0017233812250196934, 0.0017676511779427528, 0.0018117419676855206, 0.0018556505674496293,
|
||||
0.0018993609119206667, 0.0019428766099736094, 0.0019862416666001081, 0.0020295341964811087,
|
||||
0.0020728406962007284, 0.0021162291523069143, 0.0021597379818558693, 0.0022033930290490389,
|
||||
0.0022472396958619356, 0.0022913739085197449, 0.0023359460756182671, 0.0023811329156160355,
|
||||
0.0024270866997539997, 0.0024738919455558062, 0.0025215502828359604, 0.0025700139813125134,
|
||||
0.0026192441582679749, 0.0026692659594118595, 0.002720177173614502, 0.0027720888610929251,
|
||||
0.0028250094037503004, 0.0028787164483219385, 0.0029326770454645157, 0.0029860674403607845,
|
||||
0.0030379060190171003, 0.0030872693751007318, 0.0031335193198174238, 0.0031764607410877943,
|
||||
0.0032163741998374462, 0.0032539025414735079, 0.0032898378558456898, 0.0033248732797801495,
|
||||
0.003359407652169466, 0.0033934540115296841, 0.0034266682341694832, 0.0034584659151732922,
|
||||
0.0034881711471825838, 0.0035151413176208735, 0.0035388274118304253, 0.0035587677266448736,
|
||||
0.0035745392087846994, 0.0035856980830430984, 0.0035917432978749275, 0.0035921167582273483,
|
||||
0.0035862282384186983, 0.0035734928678721189, 0.0035533567424863577, 0.0035253004170954227,
|
||||
0.0034888240043073893, 0.003443423192948103, 0.0033885682933032513, 0.0033236993476748466,
|
||||
0.0032482317183166742, 0.0031615688931196928, 0.0030631136614829302, 0.0029522709082812071,
|
||||
0.0028284420259296894, 0.0026910160668194294, 0.0025393660180270672, 0.0023728485684841871,
|
||||
0.0021908141206949949, 0.0019926181994378567, 0.0017776311142370105, 0.0015452421503141522,
|
||||
0.001294855959713459, 0.0010258855763822794, 0.00073774566408246756, 0.00042984966421499848,
|
||||
0.00010161137470277026, -0.00024754938203841448, -0.00061819725669920444, -0.0010108760325238109,
|
||||
-0.001426108181476593, -0.0018643926596269011, -0.0023262077011168003, -0.002812013728544116,
|
||||
-0.0033222525380551815, -0.003857344388961792, -0.0044176783412694931, -0.0050036045722663403,
|
||||
-0.0056154225021600723, -0.0062533821910619736, -0.0069176913239061832, -0.0076085370965301991,
|
||||
-0.008326113224029541, -0.0090706516057252884, -0.0098424339666962624, -0.010641784407198429,
|
||||
-0.011469035409390926, -0.012324465438723564, -0.013208229094743729, -0.014120301231741905,
|
||||
0.015060451813042164, 0.016028247773647308, 0.017023105174303055, 0.018044359982013702,
|
||||
0.019091326743364334, 0.020163353532552719, 0.021259821951389313, 0.022380130365490913,
|
||||
0.02352365106344223, 0.024689681828022003, 0.025877414271235466, 0.027085918933153152,
|
||||
0.028314167633652687, 0.029561035335063934, 0.030825328081846237, 0.032105788588523865,
|
||||
0.033401083201169968, 0.034709792584180832, 0.036030396819114685, 0.037361271679401398,
|
||||
0.03870067372918129, 0.040046781301498413, 0.041397668421268463, 0.042751342058181763,
|
||||
0.044105727225542068, 0.045458663254976273, 0.046807888895273209, 0.048151064664125443,
|
||||
0.049485750496387482, 0.050809424370527267, 0.05211947113275528, 0.053413204848766327,
|
||||
0.054687850177288055, 0.055940557271242142, 0.057168368250131607, 0.058368254452943802,
|
||||
0.059537097811698914, 0.06067170575261116, 0.061768818646669388, 0.062825113534927368,
|
||||
0.063837200403213501, 0.064801648259162903, 0.065714947879314423, 0.066573545336723328,
|
||||
0.067373812198638916, 0.068112112581729889, 0.068784743547439575, 0.069387979805469513,
|
||||
0.069918066263198853, 0.070371203124523163, 0.070743560791015625, 0.071031264960765839,
|
||||
0.071230456233024597, 0.071337237954139709, 0.071347743272781372, 0.071258097887039185,
|
||||
0.07106444239616394, 0.070762887597084045, 0.070349536836147308, 0.069820456206798553,
|
||||
0.069171726703643799, 0.068399444222450256, 0.067499779164791107, 0.066468983888626099,
|
||||
0.065303429961204529, 0.063999593257904053, 0.062554046511650085, 0.060963429510593414,
|
||||
0.059224434196949005, 0.057333782315254211, 0.055288247764110565, 0.05308464914560318,
|
||||
0.050719890743494034, 0.04819098487496376, 0.045495055615901947, 0.042629346251487732,
|
||||
0.039591230452060699, 0.036378197371959686, 0.032987859100103378, 0.029417969286441803,
|
||||
0.025666400790214539, 0.0217311792075634, 0.017610486596822739, 0.013302664272487164,
|
||||
0.0088062174618244171, 0.0041198157705366611, -0.00075770384864881635, -0.0058273370377719402,
|
||||
-0.011089906096458435, -0.016546055674552917, -0.022196246311068535, -0.028040755540132523,
|
||||
-0.034079667180776596, -0.040312871336936951, -0.046740073710680008, -0.053360763937234879,
|
||||
-0.06017424538731575, -0.0671796053647995, -0.074375726282596588, -0.081761270761489868,
|
||||
-0.089334696531295776, -0.097094230353832245, -0.10503791272640228, -0.1131635457277298,
|
||||
-0.12146873027086258, -0.12995083630084991, -0.13860704004764557, -0.1474342942237854,
|
||||
-0.15642932057380676, -0.1655886322259903, -0.17490856349468231, -0.18438516557216644,
|
||||
-0.19401434063911438, -0.20379173755645752, -0.21371282637119293, -0.22377283871173859,
|
||||
-0.23396681249141693, -0.24428960680961609, -0.25473582744598389, -0.26529994606971741,
|
||||
-0.27597621083259583, -0.2867586612701416, -0.29764124751091003, -0.30861768126487732,
|
||||
-0.31968152523040771, -0.33082622289657593, -0.34204500913619995, -0.35333094000816345,
|
||||
0.36467701196670532, 0.37607598304748535, 0.38752046227455139, 0.39900293946266174,
|
||||
0.41051584482192993, 0.42205137014389038, 0.4336017370223999, 0.44515895843505859,
|
||||
0.45671501755714417, 0.46826183795928955, 0.47979119420051575, 0.49129492044448853,
|
||||
0.50276464223861694, 0.51419198513031006, 0.52556860446929932, 0.53688603639602661,
|
||||
0.54813587665557861, 0.55930960178375244, 0.57039880752563477, 0.58139497041702271,
|
||||
0.59228962659835815, 0.60307443141937256, 0.61374092102050781, 0.62428075075149536,
|
||||
0.63468557596206665, 0.64494723081588745, 0.65505754947662354, 0.66500836610794067,
|
||||
0.67479169368743896, 0.68439966440200806, 0.69382447004318237, 0.70305842161178589,
|
||||
0.71209394931793213, 0.72092366218566895, 0.72954028844833374, 0.73793661594390869,
|
||||
0.74610573053359985, 0.75404083728790283, 0.76173526048660278, 0.76918256282806396,
|
||||
0.77637648582458496, 0.78331100940704346, 0.78998017311096191, 0.79637837409973145,
|
||||
0.80250018835067749, 0.80834043025970459, 0.81389403343200684, 0.81915634870529175,
|
||||
0.82412278652191162, 0.82878917455673218, 0.83315145969390869, 0.83720588684082031,
|
||||
0.84094899892807007, 0.84437751770019531, 0.84748858213424683, 0.85027939081192017,
|
||||
0.85274755954742432, 0.85489106178283691, 0.85670793056488037, 0.8581966757774353,
|
||||
0.85935592651367188, 0.86018466949462891, 0.86068224906921387, 0.86084812879562378,
|
||||
0.86068224906921387, 0.86018466949462891, 0.85935592651367188, 0.8581966757774353,
|
||||
0.85670793056488037, 0.85489106178283691, 0.85274755954742432, 0.85027939081192017,
|
||||
0.84748858213424683, 0.84437751770019531, 0.84094899892807007, 0.83720588684082031,
|
||||
0.83315145969390869, 0.82878917455673218, 0.82412278652191162, 0.81915634870529175,
|
||||
0.81389403343200684, 0.80834043025970459, 0.80250018835067749, 0.79637837409973145,
|
||||
0.78998017311096191, 0.78331100940704346, 0.77637648582458496, 0.76918256282806396,
|
||||
0.76173526048660278, 0.75404083728790283, 0.74610573053359985, 0.73793661594390869,
|
||||
0.72954028844833374, 0.72092366218566895, 0.71209394931793213, 0.70305842161178589,
|
||||
0.69382447004318237, 0.68439966440200806, 0.67479169368743896, 0.66500836610794067,
|
||||
0.65505754947662354, 0.64494723081588745, 0.63468557596206665, 0.62428075075149536,
|
||||
0.61374092102050781, 0.60307443141937256, 0.59228962659835815, 0.58139497041702271,
|
||||
0.57039880752563477, 0.55930960178375244, 0.54813587665557861, 0.53688603639602661,
|
||||
0.52556860446929932, 0.51419198513031006, 0.50276464223861694, 0.49129492044448853,
|
||||
0.47979119420051575, 0.46826183795928955, 0.45671501755714417, 0.44515895843505859,
|
||||
0.4336017370223999, 0.42205137014389038, 0.41051584482192993, 0.39900293946266174,
|
||||
0.38752046227455139, 0.37607598304748535, 0.36467701196670532, 0.35333094000816345,
|
||||
-0.34204500913619995, -0.33082622289657593, -0.31968152523040771, -0.30861768126487732,
|
||||
-0.29764124751091003, -0.2867586612701416, -0.27597621083259583, -0.26529994606971741,
|
||||
-0.25473582744598389, -0.24428960680961609, -0.23396681249141693, -0.22377283871173859,
|
||||
-0.21371282637119293, -0.20379173755645752, -0.19401434063911438, -0.18438516557216644,
|
||||
-0.17490856349468231, -0.1655886322259903, -0.15642932057380676, -0.1474342942237854,
|
||||
-0.13860704004764557, -0.12995083630084991, -0.12146873027086258, -0.1131635457277298,
|
||||
-0.10503791272640228, -0.097094230353832245, -0.089334696531295776, -0.081761270761489868,
|
||||
-0.074375726282596588, -0.0671796053647995, -0.06017424538731575, -0.053360763937234879,
|
||||
-0.046740073710680008, -0.040312871336936951, -0.034079667180776596, -0.028040755540132523,
|
||||
-0.022196246311068535, -0.016546055674552917, -0.011089906096458435, -0.0058273370377719402,
|
||||
-0.00075770384864881635, 0.0041198157705366611, 0.0088062174618244171, 0.013302664272487164,
|
||||
0.017610486596822739, 0.0217311792075634, 0.025666400790214539, 0.029417969286441803,
|
||||
0.032987859100103378, 0.036378197371959686, 0.039591230452060699, 0.042629346251487732,
|
||||
0.045495055615901947, 0.04819098487496376, 0.050719890743494034, 0.05308464914560318,
|
||||
0.055288247764110565, 0.057333782315254211, 0.059224434196949005, 0.060963429510593414,
|
||||
0.062554046511650085, 0.063999593257904053, 0.065303429961204529, 0.066468983888626099,
|
||||
0.067499779164791107, 0.068399444222450256, 0.069171726703643799, 0.069820456206798553,
|
||||
0.070349536836147308, 0.070762887597084045, 0.07106444239616394, 0.071258097887039185,
|
||||
0.071347743272781372, 0.071337237954139709, 0.071230456233024597, 0.071031264960765839,
|
||||
0.070743560791015625, 0.070371203124523163, 0.069918066263198853, 0.069387979805469513,
|
||||
0.068784743547439575, 0.068112112581729889, 0.067373812198638916, 0.066573545336723328,
|
||||
0.065714947879314423, 0.064801648259162903, 0.063837200403213501, 0.062825113534927368,
|
||||
0.061768818646669388, 0.06067170575261116, 0.059537097811698914, 0.058368254452943802,
|
||||
0.057168368250131607, 0.055940557271242142, 0.054687850177288055, 0.053413204848766327,
|
||||
0.05211947113275528, 0.050809424370527267, 0.049485750496387482, 0.048151064664125443,
|
||||
0.046807888895273209, 0.045458663254976273, 0.044105727225542068, 0.042751342058181763,
|
||||
0.041397668421268463, 0.040046781301498413, 0.03870067372918129, 0.037361271679401398,
|
||||
0.036030396819114685, 0.034709792584180832, 0.033401083201169968, 0.032105788588523865,
|
||||
0.030825328081846237, 0.029561035335063934, 0.028314167633652687, 0.027085918933153152,
|
||||
0.025877414271235466, 0.024689681828022003, 0.02352365106344223, 0.022380130365490913,
|
||||
0.021259821951389313, 0.020163353532552719, 0.019091326743364334, 0.018044359982013702,
|
||||
0.017023105174303055, 0.016028247773647308, 0.015060451813042164, 0.014120301231741905,
|
||||
-0.013208229094743729, -0.012324465438723564, -0.011469035409390926, -0.010641784407198429,
|
||||
-0.0098424339666962624, -0.0090706516057252884, -0.008326113224029541, -0.0076085370965301991,
|
||||
-0.0069176913239061832, -0.0062533821910619736, -0.0056154225021600723, -0.0050036045722663403,
|
||||
-0.0044176783412694931, -0.003857344388961792, -0.0033222525380551815, -0.002812013728544116,
|
||||
-0.0023262077011168003, -0.0018643926596269011, -0.001426108181476593, -0.0010108760325238109,
|
||||
-0.00061819725669920444, -0.00024754938203841448, 0.00010161137470277026, 0.00042984966421499848,
|
||||
0.00073774566408246756, 0.0010258855763822794, 0.001294855959713459, 0.0015452421503141522,
|
||||
0.0017776311142370105, 0.0019926181994378567, 0.0021908141206949949, 0.0023728485684841871,
|
||||
0.0025393660180270672, 0.0026910160668194294, 0.0028284420259296894, 0.0029522709082812071,
|
||||
0.0030631136614829302, 0.0031615688931196928, 0.0032482317183166742, 0.0033236993476748466,
|
||||
0.0033885682933032513, 0.003443423192948103, 0.0034888240043073893, 0.0035253004170954227,
|
||||
0.0035533567424863577, 0.0035734928678721189, 0.0035862282384186983, 0.0035921167582273483,
|
||||
0.0035917432978749275, 0.0035856980830430984, 0.0035745392087846994, 0.0035587677266448736,
|
||||
0.0035388274118304253, 0.0035151413176208735, 0.0034881711471825838, 0.0034584659151732922,
|
||||
0.0034266682341694832, 0.0033934540115296841, 0.003359407652169466, 0.0033248732797801495,
|
||||
0.0032898378558456898, 0.0032539025414735079, 0.0032163741998374462, 0.0031764607410877943,
|
||||
0.0031335193198174238, 0.0030872693751007318, 0.0030379060190171003, 0.0029860674403607845,
|
||||
0.0029326770454645157, 0.0028787164483219385, 0.0028250094037503004, 0.0027720888610929251,
|
||||
0.002720177173614502, 0.0026692659594118595, 0.0026192441582679749, 0.0025700139813125134,
|
||||
0.0025215502828359604, 0.0024738919455558062, 0.0024270866997539997, 0.0023811329156160355,
|
||||
0.0023359460756182671, 0.0022913739085197449, 0.0022472396958619356, 0.0022033930290490389,
|
||||
0.0021597379818558693, 0.0021162291523069143, 0.0020728406962007284, 0.0020295341964811087,
|
||||
0.0019862416666001081, 0.0019428766099736094, 0.0018993609119206667, 0.0018556505674496293,
|
||||
0.0018117419676855206, 0.0017676511779427528, 0.0017233812250196934, 0.0016788924112915993,
|
||||
0.0016340982401743531, 0.0015888890484347939, 0.0015431707724928856, 0.0014968989416956902,
|
||||
0.0014500867109745741, 0.0014027846045792103, 0.0013550462899729609, 0.0013069023843854666,
|
||||
0.0012583627831190825, 0.0012094489065930247, 0.0011602368904277682, 0.0011108826147392392,
|
||||
0.0010616052895784378, 0.0010126305278390646, 0.00096411682898178697, 0.00091610715026035905,
|
||||
0.00086853635730221868, 0.00082129903603345156, 0.00077434181002900004, 0.00072772568091750145,
|
||||
0.00068160606315359473, 0.0006361178238876164, 0.00059120741207152605, 0.00054649583762511611,
|
||||
0.00050126801943406463, 0.00045464080176316202, 0.0004058915947098285, 0.00035484600812196732,
|
||||
0.00030217695166356862, 0.00024947625934146345, 0.00019903187057934701, 0,
|
||||
};
|
||||
|
||||
inline constexpr double kQmf5Window[640] = {
|
||||
0, 0.00019903187057934701, 0.00024947625934146345, 0.00030217695166356862,
|
||||
0.00035484600812196732, 0.0004058915947098285, 0.00045464080176316202, 0.00050126801943406463,
|
||||
0.00054649583762511611, 0.00059120741207152605, 0.0006361178238876164, 0.00068160606315359473,
|
||||
0.00072772568091750145, 0.00077434181002900004, 0.00082129903603345156, 0.00086853635730221868,
|
||||
0.00091610715026035905, 0.00096411682898178697, 0.0010126305278390646, 0.0010616052895784378,
|
||||
0.0011108826147392392, 0.0011602368904277682, 0.0012094489065930247, 0.0012583627831190825,
|
||||
0.0013069023843854666, 0.0013550462899729609, 0.0014027846045792103, 0.0014500867109745741,
|
||||
0.0014968989416956902, 0.0015431707724928856, 0.0015888890484347939, 0.0016340982401743531,
|
||||
0.0016788924112915993, 0.0017233812250196934, 0.0017676511779427528, 0.0018117419676855206,
|
||||
0.0018556505674496293, 0.0018993609119206667, 0.0019428766099736094, 0.0019862416666001081,
|
||||
0.0020295341964811087, 0.0020728406962007284, 0.0021162291523069143, 0.0021597379818558693,
|
||||
0.0022033930290490389, 0.0022472396958619356, 0.0022913739085197449, 0.0023359460756182671,
|
||||
0.0023811329156160355, 0.0024270866997539997, 0.0024738919455558062, 0.0025215502828359604,
|
||||
0.0025700139813125134, 0.0026192441582679749, 0.0026692659594118595, 0.002720177173614502,
|
||||
0.0027720888610929251, 0.0028250094037503004, 0.0028787164483219385, 0.0029326770454645157,
|
||||
0.0029860674403607845, 0.0030379060190171003, 0.0030872693751007318, 0.0031335193198174238,
|
||||
0.0031764607410877943, 0.0032163741998374462, 0.0032539025414735079, 0.0032898378558456898,
|
||||
0.0033248732797801495, 0.003359407652169466, 0.0033934540115296841, 0.0034266682341694832,
|
||||
0.0034584659151732922, 0.0034881711471825838, 0.0035151413176208735, 0.0035388274118304253,
|
||||
0.0035587677266448736, 0.0035745392087846994, 0.0035856980830430984, 0.0035917432978749275,
|
||||
0.0035921167582273483, 0.0035862282384186983, 0.0035734928678721189, 0.0035533567424863577,
|
||||
0.0035253004170954227, 0.0034888240043073893, 0.003443423192948103, 0.0033885682933032513,
|
||||
0.0033236993476748466, 0.0032482317183166742, 0.0031615688931196928, 0.0030631136614829302,
|
||||
0.0029522709082812071, 0.0028284420259296894, 0.0026910160668194294, 0.0025393660180270672,
|
||||
0.0023728485684841871, 0.0021908141206949949, 0.0019926181994378567, 0.0017776311142370105,
|
||||
0.0015452421503141522, 0.001294855959713459, 0.0010258855763822794, 0.00073774566408246756,
|
||||
0.00042984966421499848, 0.00010161137470277026, -0.00024754938203841448, -0.00061819725669920444,
|
||||
-0.0010108760325238109, -0.001426108181476593, -0.0018643926596269011, -0.0023262077011168003,
|
||||
-0.002812013728544116, -0.0033222525380551815, -0.003857344388961792, -0.0044176783412694931,
|
||||
-0.0050036045722663403, -0.0056154225021600723, -0.0062533821910619736, -0.0069176913239061832,
|
||||
-0.0076085370965301991, -0.008326113224029541, -0.0090706516057252884, -0.0098424339666962624,
|
||||
-0.010641784407198429, -0.011469035409390926, -0.012324465438723564, -0.013208229094743729,
|
||||
0.014120301231741905, 0.015060451813042164, 0.016028247773647308, 0.017023105174303055,
|
||||
0.018044359982013702, 0.019091326743364334, 0.020163353532552719, 0.021259821951389313,
|
||||
0.022380130365490913, 0.02352365106344223, 0.024689681828022003, 0.025877414271235466,
|
||||
0.027085918933153152, 0.028314167633652687, 0.029561035335063934, 0.030825328081846237,
|
||||
0.032105788588523865, 0.033401083201169968, 0.034709792584180832, 0.036030396819114685,
|
||||
0.037361271679401398, 0.03870067372918129, 0.040046781301498413, 0.041397668421268463,
|
||||
0.042751342058181763, 0.044105727225542068, 0.045458663254976273, 0.046807888895273209,
|
||||
0.048151064664125443, 0.049485750496387482, 0.050809424370527267, 0.05211947113275528,
|
||||
0.053413204848766327, 0.054687850177288055, 0.055940557271242142, 0.057168368250131607,
|
||||
0.058368254452943802, 0.059537097811698914, 0.06067170575261116, 0.061768818646669388,
|
||||
0.062825113534927368, 0.063837200403213501, 0.064801648259162903, 0.065714947879314423,
|
||||
0.066573545336723328, 0.067373812198638916, 0.068112112581729889, 0.068784743547439575,
|
||||
0.069387979805469513, 0.069918066263198853, 0.070371203124523163, 0.070743560791015625,
|
||||
0.071031264960765839, 0.071230456233024597, 0.071337237954139709, 0.071347743272781372,
|
||||
0.071258097887039185, 0.07106444239616394, 0.070762887597084045, 0.070349536836147308,
|
||||
0.069820456206798553, 0.069171726703643799, 0.068399444222450256, 0.067499779164791107,
|
||||
0.066468983888626099, 0.065303429961204529, 0.063999593257904053, 0.062554046511650085,
|
||||
0.060963429510593414, 0.059224434196949005, 0.057333782315254211, 0.055288247764110565,
|
||||
0.05308464914560318, 0.050719890743494034, 0.04819098487496376, 0.045495055615901947,
|
||||
0.042629346251487732, 0.039591230452060699, 0.036378197371959686, 0.032987859100103378,
|
||||
0.029417969286441803, 0.025666400790214539, 0.0217311792075634, 0.017610486596822739,
|
||||
0.013302664272487164, 0.0088062174618244171, 0.0041198157705366611, -0.00075770384864881635,
|
||||
-0.0058273370377719402, -0.011089906096458435, -0.016546055674552917, -0.022196246311068535,
|
||||
-0.028040755540132523, -0.034079667180776596, -0.040312871336936951, -0.046740073710680008,
|
||||
-0.053360763937234879, -0.06017424538731575, -0.0671796053647995, -0.074375726282596588,
|
||||
-0.081761270761489868, -0.089334696531295776, -0.097094230353832245, -0.10503791272640228,
|
||||
-0.1131635457277298, -0.12146873027086258, -0.12995083630084991, -0.13860704004764557,
|
||||
-0.1474342942237854, -0.15642932057380676, -0.1655886322259903, -0.17490856349468231,
|
||||
-0.18438516557216644, -0.19401434063911438, -0.20379173755645752, -0.21371282637119293,
|
||||
-0.22377283871173859, -0.23396681249141693, -0.24428960680961609, -0.25473582744598389,
|
||||
-0.26529994606971741, -0.27597621083259583, -0.2867586612701416, -0.29764124751091003,
|
||||
-0.30861768126487732, -0.31968152523040771, -0.33082622289657593, -0.34204500913619995,
|
||||
0.35333094000816345, 0.36467701196670532, 0.37607598304748535, 0.38752046227455139,
|
||||
0.39900293946266174, 0.41051584482192993, 0.42205137014389038, 0.4336017370223999,
|
||||
0.44515895843505859, 0.45671501755714417, 0.46826183795928955, 0.47979119420051575,
|
||||
0.49129492044448853, 0.50276464223861694, 0.51419198513031006, 0.52556860446929932,
|
||||
0.53688603639602661, 0.54813587665557861, 0.55930960178375244, 0.57039880752563477,
|
||||
0.58139497041702271, 0.59228962659835815, 0.60307443141937256, 0.61374092102050781,
|
||||
0.62428075075149536, 0.63468557596206665, 0.64494723081588745, 0.65505754947662354,
|
||||
0.66500836610794067, 0.67479169368743896, 0.68439966440200806, 0.69382447004318237,
|
||||
0.70305842161178589, 0.71209394931793213, 0.72092366218566895, 0.72954028844833374,
|
||||
0.73793661594390869, 0.74610573053359985, 0.75404083728790283, 0.76173526048660278,
|
||||
0.76918256282806396, 0.77637648582458496, 0.78331100940704346, 0.78998017311096191,
|
||||
0.79637837409973145, 0.80250018835067749, 0.80834043025970459, 0.81389403343200684,
|
||||
0.81915634870529175, 0.82412278652191162, 0.82878917455673218, 0.83315145969390869,
|
||||
0.83720588684082031, 0.84094899892807007, 0.84437751770019531, 0.84748858213424683,
|
||||
0.85027939081192017, 0.85274755954742432, 0.85489106178283691, 0.85670793056488037,
|
||||
0.8581966757774353, 0.85935592651367188, 0.86018466949462891, 0.86068224906921387,
|
||||
0.86084812879562378, 0.86068224906921387, 0.86018466949462891, 0.85935592651367188,
|
||||
0.8581966757774353, 0.85670793056488037, 0.85489106178283691, 0.85274755954742432,
|
||||
0.85027939081192017, 0.84748858213424683, 0.84437751770019531, 0.84094899892807007,
|
||||
0.83720588684082031, 0.83315145969390869, 0.82878917455673218, 0.82412278652191162,
|
||||
0.81915634870529175, 0.81389403343200684, 0.80834043025970459, 0.80250018835067749,
|
||||
0.79637837409973145, 0.78998017311096191, 0.78331100940704346, 0.77637648582458496,
|
||||
0.76918256282806396, 0.76173526048660278, 0.75404083728790283, 0.74610573053359985,
|
||||
0.73793661594390869, 0.72954028844833374, 0.72092366218566895, 0.71209394931793213,
|
||||
0.70305842161178589, 0.69382447004318237, 0.68439966440200806, 0.67479169368743896,
|
||||
0.66500836610794067, 0.65505754947662354, 0.64494723081588745, 0.63468557596206665,
|
||||
0.62428075075149536, 0.61374092102050781, 0.60307443141937256, 0.59228962659835815,
|
||||
0.58139497041702271, 0.57039880752563477, 0.55930960178375244, 0.54813587665557861,
|
||||
0.53688603639602661, 0.52556860446929932, 0.51419198513031006, 0.50276464223861694,
|
||||
0.49129492044448853, 0.47979119420051575, 0.46826183795928955, 0.45671501755714417,
|
||||
0.44515895843505859, 0.4336017370223999, 0.42205137014389038, 0.41051584482192993,
|
||||
0.39900293946266174, 0.38752046227455139, 0.37607598304748535, 0.36467701196670532,
|
||||
-0.35333094000816345, -0.34204500913619995, -0.33082622289657593, -0.31968152523040771,
|
||||
-0.30861768126487732, -0.29764124751091003, -0.2867586612701416, -0.27597621083259583,
|
||||
-0.26529994606971741, -0.25473582744598389, -0.24428960680961609, -0.23396681249141693,
|
||||
-0.22377283871173859, -0.21371282637119293, -0.20379173755645752, -0.19401434063911438,
|
||||
-0.18438516557216644, -0.17490856349468231, -0.1655886322259903, -0.15642932057380676,
|
||||
-0.1474342942237854, -0.13860704004764557, -0.12995083630084991, -0.12146873027086258,
|
||||
-0.1131635457277298, -0.10503791272640228, -0.097094230353832245, -0.089334696531295776,
|
||||
-0.081761270761489868, -0.074375726282596588, -0.0671796053647995, -0.06017424538731575,
|
||||
-0.053360763937234879, -0.046740073710680008, -0.040312871336936951, -0.034079667180776596,
|
||||
-0.028040755540132523, -0.022196246311068535, -0.016546055674552917, -0.011089906096458435,
|
||||
-0.0058273370377719402, -0.00075770384864881635, 0.0041198157705366611, 0.0088062174618244171,
|
||||
0.013302664272487164, 0.017610486596822739, 0.0217311792075634, 0.025666400790214539,
|
||||
0.029417969286441803, 0.032987859100103378, 0.036378197371959686, 0.039591230452060699,
|
||||
0.042629346251487732, 0.045495055615901947, 0.04819098487496376, 0.050719890743494034,
|
||||
0.05308464914560318, 0.055288247764110565, 0.057333782315254211, 0.059224434196949005,
|
||||
0.060963429510593414, 0.062554046511650085, 0.063999593257904053, 0.065303429961204529,
|
||||
0.066468983888626099, 0.067499779164791107, 0.068399444222450256, 0.069171726703643799,
|
||||
0.069820456206798553, 0.070349536836147308, 0.070762887597084045, 0.07106444239616394,
|
||||
0.071258097887039185, 0.071347743272781372, 0.071337237954139709, 0.071230456233024597,
|
||||
0.071031264960765839, 0.070743560791015625, 0.070371203124523163, 0.069918066263198853,
|
||||
0.069387979805469513, 0.068784743547439575, 0.068112112581729889, 0.067373812198638916,
|
||||
0.066573545336723328, 0.065714947879314423, 0.064801648259162903, 0.063837200403213501,
|
||||
0.062825113534927368, 0.061768818646669388, 0.06067170575261116, 0.059537097811698914,
|
||||
0.058368254452943802, 0.057168368250131607, 0.055940557271242142, 0.054687850177288055,
|
||||
0.053413204848766327, 0.05211947113275528, 0.050809424370527267, 0.049485750496387482,
|
||||
0.048151064664125443, 0.046807888895273209, 0.045458663254976273, 0.044105727225542068,
|
||||
0.042751342058181763, 0.041397668421268463, 0.040046781301498413, 0.03870067372918129,
|
||||
0.037361271679401398, 0.036030396819114685, 0.034709792584180832, 0.033401083201169968,
|
||||
0.032105788588523865, 0.030825328081846237, 0.029561035335063934, 0.028314167633652687,
|
||||
0.027085918933153152, 0.025877414271235466, 0.024689681828022003, 0.02352365106344223,
|
||||
0.022380130365490913, 0.021259821951389313, 0.020163353532552719, 0.019091326743364334,
|
||||
0.018044359982013702, 0.017023105174303055, 0.016028247773647308, 0.015060451813042164,
|
||||
-0.014120301231741905, -0.013208229094743729, -0.012324465438723564, -0.011469035409390926,
|
||||
-0.010641784407198429, -0.0098424339666962624, -0.0090706516057252884, -0.008326113224029541,
|
||||
-0.0076085370965301991, -0.0069176913239061832, -0.0062533821910619736, -0.0056154225021600723,
|
||||
-0.0050036045722663403, -0.0044176783412694931, -0.003857344388961792, -0.0033222525380551815,
|
||||
-0.002812013728544116, -0.0023262077011168003, -0.0018643926596269011, -0.001426108181476593,
|
||||
-0.0010108760325238109, -0.00061819725669920444, -0.00024754938203841448, 0.00010161137470277026,
|
||||
0.00042984966421499848, 0.00073774566408246756, 0.0010258855763822794, 0.001294855959713459,
|
||||
0.0015452421503141522, 0.0017776311142370105, 0.0019926181994378567, 0.0021908141206949949,
|
||||
0.0023728485684841871, 0.0025393660180270672, 0.0026910160668194294, 0.0028284420259296894,
|
||||
0.0029522709082812071, 0.0030631136614829302, 0.0031615688931196928, 0.0032482317183166742,
|
||||
0.0033236993476748466, 0.0033885682933032513, 0.003443423192948103, 0.0034888240043073893,
|
||||
0.0035253004170954227, 0.0035533567424863577, 0.0035734928678721189, 0.0035862282384186983,
|
||||
0.0035921167582273483, 0.0035917432978749275, 0.0035856980830430984, 0.0035745392087846994,
|
||||
0.0035587677266448736, 0.0035388274118304253, 0.0035151413176208735, 0.0034881711471825838,
|
||||
0.0034584659151732922, 0.0034266682341694832, 0.0033934540115296841, 0.003359407652169466,
|
||||
0.0033248732797801495, 0.0032898378558456898, 0.0032539025414735079, 0.0032163741998374462,
|
||||
0.0031764607410877943, 0.0031335193198174238, 0.0030872693751007318, 0.0030379060190171003,
|
||||
0.0029860674403607845, 0.0029326770454645157, 0.0028787164483219385, 0.0028250094037503004,
|
||||
0.0027720888610929251, 0.002720177173614502, 0.0026692659594118595, 0.0026192441582679749,
|
||||
0.0025700139813125134, 0.0025215502828359604, 0.0024738919455558062, 0.0024270866997539997,
|
||||
0.0023811329156160355, 0.0023359460756182671, 0.0022913739085197449, 0.0022472396958619356,
|
||||
0.0022033930290490389, 0.0021597379818558693, 0.0021162291523069143, 0.0020728406962007284,
|
||||
0.0020295341964811087, 0.0019862416666001081, 0.0019428766099736094, 0.0018993609119206667,
|
||||
0.0018556505674496293, 0.0018117419676855206, 0.0017676511779427528, 0.0017233812250196934,
|
||||
0.0016788924112915993, 0.0016340982401743531, 0.0015888890484347939, 0.0015431707724928856,
|
||||
0.0014968989416956902, 0.0014500867109745741, 0.0014027846045792103, 0.0013550462899729609,
|
||||
0.0013069023843854666, 0.0012583627831190825, 0.0012094489065930247, 0.0011602368904277682,
|
||||
0.0011108826147392392, 0.0010616052895784378, 0.0010126305278390646, 0.00096411682898178697,
|
||||
0.00091610715026035905, 0.00086853635730221868, 0.00082129903603345156, 0.00077434181002900004,
|
||||
0.00072772568091750145, 0.00068160606315359473, 0.0006361178238876164, 0.00059120741207152605,
|
||||
0.00054649583762511611, 0.00050126801943406463, 0.00045464080176316202, 0.0004058915947098285,
|
||||
0.00035484600812196732, 0.00030217695166356862, 0.00024947625934146345, 0.00019903187057934701,
|
||||
};
|
||||
|
||||
inline constexpr uint8_t kPbMap1[64] = {
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
};
|
||||
|
||||
inline constexpr uint8_t kPbMap3[64] = {
|
||||
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
};
|
||||
|
||||
inline constexpr uint8_t kPbMap5[64] = {
|
||||
0, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
|
||||
3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
};
|
||||
|
||||
inline constexpr uint8_t kPbMap7[64] = {
|
||||
0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5,
|
||||
5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
};
|
||||
|
||||
inline constexpr uint8_t kPbMap9[64] = {
|
||||
0, 1, 2, 3, 3, 3, 4, 5, 5, 6, 6, 6, 7, 7, 8, 8,
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
||||
};
|
||||
|
||||
inline constexpr uint8_t kPbMap12[64] = {
|
||||
0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8,
|
||||
8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10,
|
||||
10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
|
||||
11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
|
||||
};
|
||||
|
||||
inline constexpr uint8_t kPbMap15[64] = {
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 10, 10, 11, 11, 11,
|
||||
12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14,
|
||||
14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
|
||||
14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
|
||||
};
|
||||
|
||||
inline constexpr uint8_t kPbMap23[64] = {
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 12, 13, 13,
|
||||
14, 14, 15, 15, 16, 16, 16, 17, 17, 17, 18, 18, 18, 18, 19, 19,
|
||||
19, 19, 19, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21,
|
||||
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
|
||||
};
|
||||
} // namespace ejoc::tables
|
||||
@@ -0,0 +1,500 @@
|
||||
#include "oamd/oamd_parser.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
#include "foundation/bit_reader.h"
|
||||
|
||||
namespace joc::oamd {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kSampleOffsetIndex[4] = {8, 16, 18, 24};
|
||||
constexpr int kRampDurations[3] = {0, 512, 1536};
|
||||
constexpr int kRampDurationIndex[16] = {32, 64, 128, 256, 320, 480,
|
||||
1000, 1001, 1024, 1600, 1601, 1602,
|
||||
1920, 2000, 2002, 2048};
|
||||
constexpr std::uint32_t kObjectElementId = 1;
|
||||
constexpr int kIsfObjectCounts[6] = {4, 8, 10, 14, 15, 30};
|
||||
constexpr int kStandardBedChannelCount[10] = {2, 1, 1, 2, 2, 2, 2, 2, 2, 1};
|
||||
|
||||
Status variant(const std::string& name, const std::string& message) {
|
||||
return Status::fail(JOC_ERR_OAMD_UNSUPPORTED_VARIANT, stage::kOamd,
|
||||
name + ": " + message);
|
||||
}
|
||||
|
||||
Status syntax(const std::string& message) {
|
||||
return Status::fail(JOC_ERR_OAMD_SYNTAX, stage::kOamd, message);
|
||||
}
|
||||
|
||||
bool variable_bits_max(bits::BitReader& reader, unsigned width, unsigned max_groups,
|
||||
std::uint32_t* out_value) {
|
||||
std::uint32_t value = reader.read(width);
|
||||
if (reader.failed()) {
|
||||
return false;
|
||||
}
|
||||
std::uint32_t more = reader.read(1);
|
||||
if (reader.failed()) {
|
||||
return false;
|
||||
}
|
||||
unsigned num_group = 1;
|
||||
if (max_groups > num_group) {
|
||||
if (more != 0u) {
|
||||
value = (value + 1u) << width;
|
||||
}
|
||||
while (more != 0u) {
|
||||
value += reader.read(width);
|
||||
more = reader.read(1);
|
||||
if (reader.failed()) {
|
||||
return false;
|
||||
}
|
||||
if (num_group >= max_groups) {
|
||||
break;
|
||||
}
|
||||
if (more != 0u) {
|
||||
value = (value + 1u) << width;
|
||||
num_group += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
*out_value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct ProgramInfo {
|
||||
bool dynamic_object_only = false;
|
||||
bool lfe_present = false;
|
||||
std::uint32_t num_bed_objects = 0;
|
||||
std::uint32_t num_isf_objects = 0;
|
||||
std::int32_t num_dynamic_objects = -1;
|
||||
std::vector<BedAssignment> bed_assignments;
|
||||
};
|
||||
|
||||
Status parse_program_assignment(bits::BitReader& reader, ProgramInfo* program) {
|
||||
program->dynamic_object_only = reader.read(1) != 0u;
|
||||
if (reader.failed()) {
|
||||
return syntax("program_assignment truncated");
|
||||
}
|
||||
if (program->dynamic_object_only) {
|
||||
program->lfe_present = reader.read(1) != 0u;
|
||||
if (reader.failed()) {
|
||||
return syntax("program_assignment truncated");
|
||||
}
|
||||
program->num_bed_objects = program->lfe_present ? 1u : 0u;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
const std::uint32_t mask = reader.read(4);
|
||||
if (reader.failed()) {
|
||||
return syntax("program_assignment truncated");
|
||||
}
|
||||
if ((mask & 0x1u) != 0u) {
|
||||
reader.read(1);
|
||||
const std::uint32_t multi = reader.read(1);
|
||||
if (reader.failed()) {
|
||||
return syntax("program_assignment truncated");
|
||||
}
|
||||
std::uint32_t instances = 1;
|
||||
if (multi != 0u) {
|
||||
instances = reader.read(3) + 2u;
|
||||
}
|
||||
for (std::uint32_t instance = 0; instance < instances; ++instance) {
|
||||
BedAssignment assignment;
|
||||
if (reader.read(1) != 0u) {
|
||||
assignment.lfe_only = true;
|
||||
assignment.mask = 0;
|
||||
program->bed_assignments.push_back(assignment);
|
||||
program->num_bed_objects += 1;
|
||||
continue;
|
||||
}
|
||||
if (reader.read(1) != 0u) {
|
||||
assignment.standard = true;
|
||||
assignment.mask = reader.read(10);
|
||||
for (int bit = 0; bit < 10; ++bit) {
|
||||
if (((assignment.mask >> bit) & 1u) != 0u) {
|
||||
program->num_bed_objects +=
|
||||
static_cast<std::uint32_t>(kStandardBedChannelCount[bit]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assignment.standard = false;
|
||||
assignment.mask = reader.read(17);
|
||||
for (int bit = 0; bit < 17; ++bit) {
|
||||
if (((assignment.mask >> bit) & 1u) != 0u) {
|
||||
program->num_bed_objects += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
program->bed_assignments.push_back(assignment);
|
||||
if (reader.failed()) {
|
||||
return syntax("program_assignment truncated");
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((mask & 0x2u) != 0u) {
|
||||
const std::uint32_t isf_idx = reader.read(3);
|
||||
if (reader.failed()) {
|
||||
return syntax("program_assignment truncated");
|
||||
}
|
||||
program->num_isf_objects =
|
||||
(isf_idx < 6u) ? static_cast<std::uint32_t>(kIsfObjectCounts[isf_idx]) : 0u;
|
||||
}
|
||||
if ((mask & 0x4u) != 0u) {
|
||||
std::uint32_t count = reader.read(5);
|
||||
if (count == 0x1Fu) {
|
||||
count += reader.read(7);
|
||||
}
|
||||
if (reader.failed()) {
|
||||
return syntax("program_assignment truncated");
|
||||
}
|
||||
program->num_dynamic_objects = static_cast<std::int32_t>(count + 1u);
|
||||
}
|
||||
if ((mask & 0x8u) != 0u) {
|
||||
const std::uint32_t reserved_bytes = reader.read(4) + 1u;
|
||||
if (!reader.skip(static_cast<std::size_t>(reserved_bytes) * 8u)) {
|
||||
return syntax("program_assignment reserved data truncated");
|
||||
}
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status parse_object_info_block(bits::BitReader& reader, int object_index, bool in_bed_or_isf,
|
||||
int* position_x, int* position_y, int* position_z, bool* has_position) {
|
||||
*has_position = false;
|
||||
const bool not_active = reader.read(1) != 0u;
|
||||
if (reader.failed()) {
|
||||
return syntax("object_info_block truncated");
|
||||
}
|
||||
const std::uint32_t basic_status = not_active ? 0u : 1u;
|
||||
if (basic_status == 1u) {
|
||||
const std::uint32_t gain_idx = reader.read(2);
|
||||
if (gain_idx == 2u) {
|
||||
reader.read(6);
|
||||
}
|
||||
const bool default_priority = reader.read(1) != 0u;
|
||||
if (!default_priority) {
|
||||
reader.read(5);
|
||||
}
|
||||
if (reader.failed()) {
|
||||
return syntax("object_info_block truncated");
|
||||
}
|
||||
}
|
||||
const std::uint32_t render_status = (not_active || in_bed_or_isf) ? 0u : 1u;
|
||||
if (render_status == 1u) {
|
||||
const int x = static_cast<int>(reader.read(6));
|
||||
const int y = static_cast<int>(reader.read(6));
|
||||
const int z_sign = static_cast<int>(reader.read(1));
|
||||
const int z = static_cast<int>(reader.read(4));
|
||||
if (reader.failed()) {
|
||||
return syntax("object_info_block truncated");
|
||||
}
|
||||
*position_x = x;
|
||||
*position_y = y;
|
||||
*position_z = z_sign != 0 ? z : -z;
|
||||
*has_position = true;
|
||||
if (reader.read(1) != 0u) {
|
||||
if (reader.read(1) == 0u) {
|
||||
reader.read(4);
|
||||
}
|
||||
}
|
||||
reader.read(3);
|
||||
reader.read(1);
|
||||
const std::uint32_t size_idx = reader.read(2);
|
||||
if (size_idx == 1u) {
|
||||
reader.read(5);
|
||||
} else if (size_idx == 2u) {
|
||||
reader.read(15);
|
||||
}
|
||||
if (reader.read(1) != 0u) {
|
||||
reader.read(3);
|
||||
reader.read(2);
|
||||
}
|
||||
reader.read(1);
|
||||
if (reader.failed()) {
|
||||
return syntax("object_info_block truncated");
|
||||
}
|
||||
}
|
||||
if (reader.read(1) != 0u) {
|
||||
const std::uint32_t additional_bytes = reader.read(4) + 1u;
|
||||
if (!reader.skip(static_cast<std::size_t>(additional_bytes) * 8u)) {
|
||||
return syntax("object_info_block additional table truncated");
|
||||
}
|
||||
}
|
||||
(void)object_index;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
struct ObjectElementInfo {
|
||||
std::uint32_t sample_offset_code = 0;
|
||||
std::uint32_t sample_offset = 0;
|
||||
std::uint32_t block_count = 0;
|
||||
std::uint32_t block_offset_samples = 0;
|
||||
std::uint32_t ramp_duration_samples = 0;
|
||||
bool reserved_data_not_present = false;
|
||||
};
|
||||
|
||||
Status parse_object_element(bits::BitReader& reader, std::uint32_t object_count,
|
||||
std::uint32_t bed_isf_objects, ObjectElementInfo* out, OamdUpdate* update) {
|
||||
out->sample_offset_code = reader.read(2);
|
||||
if (reader.failed()) {
|
||||
return syntax("object_element truncated");
|
||||
}
|
||||
switch (out->sample_offset_code) {
|
||||
case 0: out->sample_offset = 0; break;
|
||||
case 1: {
|
||||
const std::uint32_t index = reader.read(2);
|
||||
if (reader.failed()) {
|
||||
return syntax("object_element truncated");
|
||||
}
|
||||
out->sample_offset = static_cast<std::uint32_t>(kSampleOffsetIndex[index & 3u]);
|
||||
break;
|
||||
}
|
||||
case 2: out->sample_offset = reader.read(5); break;
|
||||
default:
|
||||
return variant("md_sample_offset_mode",
|
||||
"MD sample-offset mode " +
|
||||
std::to_string(out->sample_offset_code) +
|
||||
" is not covered by the 16-slot model");
|
||||
}
|
||||
out->block_count = reader.read(3) + 1u;
|
||||
if (reader.failed()) {
|
||||
return syntax("object_element truncated");
|
||||
}
|
||||
for (std::uint32_t block = 0; block < out->block_count; ++block) {
|
||||
const std::uint32_t block_offset_factor = reader.read(6);
|
||||
const std::uint32_t ramp_code = reader.read(2);
|
||||
std::uint32_t ramp_duration = 0;
|
||||
if (ramp_code == 3u) {
|
||||
if (reader.read(1) != 0u) {
|
||||
const std::uint32_t index = reader.read(4);
|
||||
if (reader.failed()) {
|
||||
return syntax("object_element truncated");
|
||||
}
|
||||
ramp_duration = static_cast<std::uint32_t>(kRampDurationIndex[index & 15u]);
|
||||
} else {
|
||||
ramp_duration = reader.read(11);
|
||||
}
|
||||
} else {
|
||||
ramp_duration = static_cast<std::uint32_t>(kRampDurations[ramp_code]);
|
||||
}
|
||||
if (reader.failed()) {
|
||||
return syntax("object_element truncated");
|
||||
}
|
||||
if (block == 0) {
|
||||
out->block_offset_samples = out->sample_offset + block_offset_factor * 32u;
|
||||
out->ramp_duration_samples = ramp_duration;
|
||||
}
|
||||
}
|
||||
out->reserved_data_not_present = reader.read(1) != 0u;
|
||||
if (!out->reserved_data_not_present) {
|
||||
reader.read(5);
|
||||
}
|
||||
if (reader.failed()) {
|
||||
return syntax("object_element truncated");
|
||||
}
|
||||
|
||||
for (std::uint32_t index = 0; index < object_count; ++index) {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int z = 0;
|
||||
bool has_position = false;
|
||||
const bool in_bed_or_isf = index < bed_isf_objects;
|
||||
const Status status =
|
||||
parse_object_info_block(reader, static_cast<int>(index), in_bed_or_isf, &x, &y, &z,
|
||||
&has_position);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (!has_position || index >= static_cast<std::uint32_t>(kMaxSlots)) {
|
||||
continue;
|
||||
}
|
||||
update->slots[index].q1 =
|
||||
(x >= 0 && x <= kNQ12) ? static_cast<std::int16_t>(q_of(x, kNQ12)) : static_cast<std::int16_t>(-1);
|
||||
update->slots[index].q2 =
|
||||
(y >= 0 && y <= kNQ12) ? static_cast<std::int16_t>(q_of(y, kNQ12)) : static_cast<std::int16_t>(-1);
|
||||
if (z >= 0 && z <= kNQ3) {
|
||||
update->slots[index].q3 = static_cast<std::int16_t>(q_of(z, kNQ3));
|
||||
} else if (z < 0) {
|
||||
update->slots[index].q3 = 0;
|
||||
} else {
|
||||
update->slots[index].q3 = static_cast<std::int16_t>(-1);
|
||||
}
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int q_of(int k, int n) {
|
||||
const double value = std::floor(32768.0 * static_cast<double>(k) / static_cast<double>(n) + 0.5);
|
||||
const int quantised = static_cast<int>(value);
|
||||
return quantised > 32767 ? 32767 : quantised;
|
||||
}
|
||||
|
||||
Status parse_id11(const std::uint8_t* payload, std::size_t payload_size, OamdUpdate* out) {
|
||||
return parse_id11_verbose(payload, payload_size, out, nullptr);
|
||||
}
|
||||
|
||||
Status parse_id11_verbose(const std::uint8_t* payload, std::size_t payload_size, OamdUpdate* out,
|
||||
ParseTrace* trace) {
|
||||
if (payload == nullptr || out == nullptr) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOamd, "null payload or output");
|
||||
}
|
||||
if (payload_size == 0) {
|
||||
return variant("header_truncated", "OAMD payload is empty");
|
||||
}
|
||||
*out = OamdUpdate{};
|
||||
|
||||
bits::BitReader reader(payload, payload_size);
|
||||
std::uint32_t version = reader.read(2);
|
||||
if (reader.failed()) {
|
||||
return variant("header_truncated", "OAMD payload cannot hold the 2-bit version");
|
||||
}
|
||||
if (version == 3u) {
|
||||
version += reader.read(3);
|
||||
if (reader.failed()) {
|
||||
return syntax("OAMD version extension truncated");
|
||||
}
|
||||
}
|
||||
std::uint32_t object_count_bits = reader.read(5);
|
||||
if (object_count_bits == 0x1Fu) {
|
||||
object_count_bits += reader.read(7);
|
||||
}
|
||||
if (reader.failed()) {
|
||||
return syntax("OAMD object count truncated");
|
||||
}
|
||||
const std::uint32_t object_count = object_count_bits + 1u;
|
||||
|
||||
ProgramInfo program;
|
||||
Status status = parse_program_assignment(reader, &program);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
const std::uint32_t alternate_present = reader.read(1);
|
||||
if (reader.failed()) {
|
||||
return syntax("OAMD alternate data flag truncated");
|
||||
}
|
||||
|
||||
std::uint32_t element_count = reader.read(4);
|
||||
if (element_count == 0xFu) {
|
||||
element_count += reader.read(5);
|
||||
}
|
||||
if (reader.failed()) {
|
||||
return syntax("OAMD element count truncated");
|
||||
}
|
||||
if (element_count == 0u) {
|
||||
return variant("missing_object_element", "OAMD declares no element");
|
||||
}
|
||||
const std::uint32_t bed_isf_objects = program.num_bed_objects + program.num_isf_objects;
|
||||
|
||||
bool have_object_element = false;
|
||||
ObjectElementInfo object_element;
|
||||
for (std::uint32_t ordinal = 0; ordinal < element_count; ++ordinal) {
|
||||
const std::size_t header_start = reader.position();
|
||||
const std::uint32_t element_id = reader.read(4);
|
||||
std::uint32_t size_field = 0;
|
||||
if (!variable_bits_max(reader, 4, 4, &size_field)) {
|
||||
return syntax("OAMD element header truncated at ordinal " + std::to_string(ordinal));
|
||||
}
|
||||
const std::uint32_t size_bytes = size_field + 1u;
|
||||
const std::size_t region_start = reader.position();
|
||||
const std::size_t region_end = region_start + static_cast<std::size_t>(size_bytes) * 8u;
|
||||
if (region_end > reader.limit()) {
|
||||
return variant("element_bounds",
|
||||
"element " + std::to_string(ordinal) + " (id " +
|
||||
std::to_string(element_id) + ") declares " +
|
||||
std::to_string(size_bytes) +
|
||||
" bytes, beyond the payload (header at bit " +
|
||||
std::to_string(header_start) + ")");
|
||||
}
|
||||
if (alternate_present != 0u) {
|
||||
reader.read(4);
|
||||
}
|
||||
reader.read(1);
|
||||
if (reader.failed()) {
|
||||
return syntax("OAMD element control fields truncated at ordinal " +
|
||||
std::to_string(ordinal));
|
||||
}
|
||||
|
||||
if (element_id == kObjectElementId) {
|
||||
if (have_object_element) {
|
||||
return variant("multiple_object_elements", "OAMD contains several object elements");
|
||||
}
|
||||
have_object_element = true;
|
||||
const Status parsed = parse_object_element(reader, object_count, bed_isf_objects,
|
||||
&object_element, out);
|
||||
if (!parsed.ok()) {
|
||||
return parsed;
|
||||
}
|
||||
if (trace != nullptr) {
|
||||
trace->parsed_end_bit = static_cast<std::uint32_t>(reader.position());
|
||||
}
|
||||
if (reader.position() < region_end) {
|
||||
reader.set_limit_bits(reader.limit());
|
||||
reader.skip(region_end - reader.position());
|
||||
}
|
||||
} else {
|
||||
if (!reader.skip(region_end - reader.position())) {
|
||||
return syntax("OAMD element skip past payload end");
|
||||
}
|
||||
}
|
||||
if (reader.failed()) {
|
||||
return syntax("OAMD element parse failed at ordinal " + std::to_string(ordinal));
|
||||
}
|
||||
}
|
||||
|
||||
if (!have_object_element) {
|
||||
return variant("missing_object_element", "OAMD has no object element");
|
||||
}
|
||||
|
||||
// Trailing padding must be zero over the whole remainder.
|
||||
{
|
||||
bits::BitReader tail(payload, payload_size);
|
||||
tail.reset(payload, payload_size, reader.position());
|
||||
while (tail.remaining_bits() > 0) {
|
||||
const std::size_t chunk =
|
||||
tail.remaining_bits() > 32u ? 32u : tail.remaining_bits();
|
||||
if (tail.read(static_cast<unsigned>(chunk)) != 0u) {
|
||||
return Status::fail(JOC_ERR_BITSTREAM_PADDING, stage::kOamd,
|
||||
"OAMD payload padding is not zero (from bit " +
|
||||
std::to_string(reader.position()) + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (version != 0u) {
|
||||
return variant("oamd_version",
|
||||
"OAMD syntax version " + std::to_string(version) + " is not covered");
|
||||
}
|
||||
if (object_count > static_cast<std::uint32_t>(kMaxSlots)) {
|
||||
return variant("object_count", "OAMD object count " + std::to_string(object_count) +
|
||||
" exceeds the 16-slot model");
|
||||
}
|
||||
if (object_element.block_count != 1u) {
|
||||
return variant("multiple_position_blocks",
|
||||
"OAMD frame carries " + std::to_string(object_element.block_count) +
|
||||
" position blocks; a single frame_update cannot express that");
|
||||
}
|
||||
|
||||
out->block_offset_samples = object_element.block_offset_samples;
|
||||
out->ramp_duration_samples = object_element.ramp_duration_samples;
|
||||
out->object_count = object_count;
|
||||
out->dynamic_object_only = program.dynamic_object_only;
|
||||
out->lfe_present = program.lfe_present;
|
||||
out->num_bed_objects = program.num_bed_objects;
|
||||
out->num_isf_objects = program.num_isf_objects;
|
||||
out->num_dynamic_objects = program.num_dynamic_objects;
|
||||
|
||||
if (trace != nullptr) {
|
||||
trace->bed_assignments = program.bed_assignments;
|
||||
trace->element_count = element_count;
|
||||
trace->object_element_count = have_object_element ? 1u : 0u;
|
||||
trace->sample_offset_code = object_element.sample_offset_code;
|
||||
trace->sample_offset = object_element.sample_offset;
|
||||
trace->block_count = object_element.block_count;
|
||||
trace->reserved_data_not_present = object_element.reserved_data_not_present;
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
} // namespace joc::oamd
|
||||
@@ -0,0 +1,103 @@
|
||||
// Port of src/oamd_bits.py.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "foundation/status.h"
|
||||
#include "joc_core.h"
|
||||
|
||||
namespace joc::oamd {
|
||||
|
||||
inline constexpr int kMaxSlots = 16;
|
||||
inline constexpr int kObjects = 15;
|
||||
inline constexpr int kAxes = 3;
|
||||
inline constexpr int kNQ12 = 62;
|
||||
inline constexpr int kNQ3 = 15;
|
||||
inline constexpr int kQ15Scale = 32768;
|
||||
|
||||
// (the reference's None), i.e. hold the previous position.
|
||||
struct SlotUpdate {
|
||||
std::int16_t q1 = -1;
|
||||
std::int16_t q2 = -1;
|
||||
std::int16_t q3 = -1;
|
||||
};
|
||||
|
||||
struct OamdUpdate {
|
||||
SlotUpdate slots[kMaxSlots];
|
||||
std::uint32_t block_offset_samples = 0;
|
||||
std::uint32_t ramp_duration_samples = 0;
|
||||
std::uint32_t object_count = 0;
|
||||
bool dynamic_object_only = false;
|
||||
bool lfe_present = false;
|
||||
std::uint32_t num_bed_objects = 0;
|
||||
std::uint32_t num_isf_objects = 0;
|
||||
std::int32_t num_dynamic_objects = -1;
|
||||
};
|
||||
|
||||
class OamdState {
|
||||
public:
|
||||
OamdState() { reset(); }
|
||||
|
||||
void reset() {
|
||||
for (int slot = 0; slot < kMaxSlots; ++slot) {
|
||||
for (int axis = 0; axis < kAxes; ++axis) {
|
||||
q_[slot][axis] = 0;
|
||||
}
|
||||
}
|
||||
q_[0][0] = 16384;
|
||||
q_[0][1] = 16384;
|
||||
}
|
||||
|
||||
void apply(const OamdUpdate& update) {
|
||||
for (int slot = 0; slot < kMaxSlots; ++slot) {
|
||||
const SlotUpdate& value = update.slots[slot];
|
||||
if (value.q1 >= 0) { q_[slot][0] = value.q1; }
|
||||
if (value.q2 >= 0) { q_[slot][1] = value.q2; }
|
||||
if (value.q3 >= 0) { q_[slot][2] = value.q3; }
|
||||
}
|
||||
}
|
||||
|
||||
std::int16_t q(int slot, int axis) const { return q_[slot][axis]; }
|
||||
|
||||
// Objects 1..15 as q15 triples, the layout the speaker renderer consumes.
|
||||
void object_positions_q15(std::uint16_t out[kObjects][kAxes]) const {
|
||||
for (int object = 0; object < kObjects; ++object) {
|
||||
for (int axis = 0; axis < kAxes; ++axis) {
|
||||
out[object][axis] = static_cast<std::uint16_t>(q_[object + 1][axis]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::int16_t q_[kMaxSlots][kAxes] = {};
|
||||
};
|
||||
|
||||
// q_of(k, n) = min(32767, floor(32768 k / n + 0.5)), the reference's quantiser.
|
||||
int q_of(int k, int n);
|
||||
|
||||
Status parse_id11(const std::uint8_t* payload, std::size_t payload_size, OamdUpdate* out);
|
||||
|
||||
struct BedAssignment {
|
||||
bool lfe_only = false;
|
||||
bool standard = false;
|
||||
std::uint32_t mask = 0;
|
||||
};
|
||||
|
||||
struct ParseTrace {
|
||||
std::vector<BedAssignment> bed_assignments;
|
||||
std::uint32_t element_count = 0;
|
||||
std::uint32_t object_element_count = 0;
|
||||
std::uint32_t sample_offset_code = 0;
|
||||
std::uint32_t sample_offset = 0;
|
||||
std::uint32_t block_count = 0;
|
||||
std::uint32_t parsed_end_bit = 0;
|
||||
bool reserved_data_not_present = false;
|
||||
};
|
||||
|
||||
Status parse_id11_verbose(const std::uint8_t* payload, std::size_t payload_size, OamdUpdate* out,
|
||||
ParseTrace* trace);
|
||||
|
||||
} // namespace joc::oamd
|
||||
@@ -0,0 +1,101 @@
|
||||
// CPU feature probe; see cpu_probe.h.
|
||||
//
|
||||
// Nothing here is ISA-specific code: the x86 path uses CPUID / XGETBV (MSVC) or
|
||||
// the compiler's own runtime probe (GCC/Clang, so no inline assembly), and the
|
||||
// AArch64 path reads the aux vector. That is what lets this unit stay baseline
|
||||
// and run before the dispatcher has decided anything.
|
||||
|
||||
#include "simd/cpu_probe.h"
|
||||
|
||||
#if defined(_M_X64)
|
||||
#include <immintrin.h>
|
||||
#include <intrin.h>
|
||||
#elif defined(__x86_64__)
|
||||
#include <cpuid.h>
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && (defined(__aarch64__) || defined(_M_ARM64))
|
||||
#include <sys/auxv.h>
|
||||
#endif
|
||||
|
||||
namespace joc::simd {
|
||||
namespace {
|
||||
|
||||
// ------------------------------------------------------------------- x86-64 --
|
||||
#if defined(_M_X64) || defined(__x86_64__)
|
||||
|
||||
#if defined(_M_X64)
|
||||
|
||||
// CPUID tells us what the silicon can do; XCR0 tells us whether the OS saves the
|
||||
// state the wider registers need. Both have to agree, or the first AVX
|
||||
// instruction after a context switch corrupts another thread's registers.
|
||||
CpuFeatures probe_x86() noexcept {
|
||||
CpuFeatures features;
|
||||
int regs[4] = {0, 0, 0, 0};
|
||||
__cpuid(regs, 0);
|
||||
const int max_leaf = regs[0];
|
||||
__cpuid(regs, 1);
|
||||
features.sse2 = (regs[3] & (1 << 26)) != 0;
|
||||
const bool osxsave = (regs[2] & (1 << 27)) != 0;
|
||||
const bool avx = (regs[2] & (1 << 28)) != 0;
|
||||
if (!osxsave || !avx || max_leaf < 7) {
|
||||
return features;
|
||||
}
|
||||
const unsigned long long xcr0 = _xgetbv(0);
|
||||
const bool ymm_state = (xcr0 & 0x06ull) == 0x06ull; // XMM + YMM saved
|
||||
const bool zmm_state = (xcr0 & 0xe6ull) == 0xe6ull; // + opmask / ZMM / hi16
|
||||
if (!ymm_state) {
|
||||
return features;
|
||||
}
|
||||
__cpuidex(regs, 7, 0);
|
||||
features.avx2 = (regs[1] & (1 << 5)) != 0;
|
||||
features.avx512 = zmm_state && (regs[1] & (1 << 16)) != 0; // AVX512F
|
||||
return features;
|
||||
}
|
||||
|
||||
#else // GCC/Clang on x86-64
|
||||
|
||||
// The compiler runtime performs the same CPUID + XGETBV probe (libgcc's cpuinfo
|
||||
// checks XCR0 before it reports AVX), which keeps this file free of inline
|
||||
// assembly.
|
||||
CpuFeatures probe_x86() noexcept {
|
||||
CpuFeatures features;
|
||||
features.sse2 = __builtin_cpu_supports("sse2") != 0;
|
||||
features.avx2 = __builtin_cpu_supports("avx2") != 0;
|
||||
features.avx512 = __builtin_cpu_supports("avx512f") != 0;
|
||||
return features;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
CpuFeatures probe() noexcept { return probe_x86(); }
|
||||
|
||||
// ----------------------------------------------------------------- AArch64 --
|
||||
#elif defined(__aarch64__) || defined(_M_ARM64)
|
||||
|
||||
CpuFeatures probe() noexcept {
|
||||
CpuFeatures features;
|
||||
#if defined(__linux__)
|
||||
// ASIMD is architectural for AArch64; the aux vector only confirms it.
|
||||
features.neon = (getauxval(AT_HWCAP) & (1u << 1)) != 0u;
|
||||
#else
|
||||
features.neon = true;
|
||||
#endif
|
||||
return features;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ anything else --
|
||||
#else
|
||||
|
||||
CpuFeatures probe() noexcept { return CpuFeatures{}; }
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
const CpuFeatures& cpu_features() noexcept {
|
||||
static const CpuFeatures features = probe();
|
||||
return features;
|
||||
}
|
||||
|
||||
} // namespace joc::simd
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
// "Can this CPU and this operating system actually execute ISA X?"
|
||||
//
|
||||
// Its own unit and free of policy: it reads CPUID / XGETBV / HWCAP, caches the
|
||||
// answer, and knows nothing about JOC_SIMD, the kernel table or which
|
||||
// implementation is preferred. This is the one place that can fault the process
|
||||
// if it is wrong, so it stays small enough to review on its own.
|
||||
//
|
||||
// Compiled with the baseline ISA like every other non-intrinsic unit (see
|
||||
// CMakeLists.txt): it runs before any ISA unit is reached, so it must not execute
|
||||
// a wide instruction itself.
|
||||
|
||||
namespace joc::simd {
|
||||
|
||||
// One flag per ISA the dispatcher can ask about. A flag means "usable here":
|
||||
// both the silicon and the OS state-management agree, not merely that CPUID
|
||||
// advertises the feature.
|
||||
struct CpuFeatures {
|
||||
bool sse2 = false;
|
||||
bool avx2 = false;
|
||||
bool avx512 = false;
|
||||
bool neon = false;
|
||||
};
|
||||
|
||||
// Probed once, on first use. Never throws and never reads the environment (the
|
||||
// JOC_SIMD override is the dispatcher's business).
|
||||
const CpuFeatures& cpu_features() noexcept;
|
||||
|
||||
} // namespace joc::simd
|
||||
@@ -0,0 +1,293 @@
|
||||
// Kernel dispatch: CPU features in, kernel table out.
|
||||
//
|
||||
// This file holds policy only -- which ISA is asked for, which ladder is walked,
|
||||
// which slot ends up with which implementation. The question "can this machine
|
||||
// run ISA X at all" belongs to cpu_probe.cpp, and the implementations themselves
|
||||
// belong to the one ISA unit per ISA (kernels_scalar.cpp / kernels_intrin_*.cpp).
|
||||
//
|
||||
// JOC_SIMD=auto|scalar|sse2|avx2|avx512|neon. A forced ISA that this build or
|
||||
// this machine cannot provide is reported on stderr and then ignored -- forcing a
|
||||
// path the CPU lacks would fault instead of verifying anything.
|
||||
//
|
||||
// This unit is compiled with the baseline ISA: it runs before anything else in
|
||||
// any binary that links the kernels, so it must not execute an ISA-specific
|
||||
// instruction itself.
|
||||
|
||||
#include "simd/simd.h"
|
||||
|
||||
#include "simd/cpu_probe.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
// Which ISA units the build system put into this binary. An ISA that was not
|
||||
// compiled in has no entry point to reference at all.
|
||||
#if !defined(JOC_SIMD_HAVE_SSE2)
|
||||
#define JOC_SIMD_HAVE_SSE2 0
|
||||
#endif
|
||||
#if !defined(JOC_SIMD_HAVE_AVX2)
|
||||
#define JOC_SIMD_HAVE_AVX2 0
|
||||
#endif
|
||||
#if !defined(JOC_SIMD_HAVE_AVX512)
|
||||
#define JOC_SIMD_HAVE_AVX512 0
|
||||
#endif
|
||||
#if !defined(JOC_SIMD_HAVE_NEON)
|
||||
#define JOC_SIMD_HAVE_NEON 0
|
||||
#endif
|
||||
|
||||
namespace joc::simd {
|
||||
|
||||
// Implemented by the per-ISA units; referenced only when compiled in. The file
|
||||
// name carries the ISA (FLAC's `*_intrin_<isa>.c` convention), and the CMake list
|
||||
// gives exactly those units their wider /arch or -m flag.
|
||||
const Kernels& kernels_scalar() noexcept;
|
||||
#if JOC_SIMD_HAVE_AVX2
|
||||
const Kernels& kernels_intrin_avx2() noexcept;
|
||||
#endif
|
||||
#if JOC_SIMD_HAVE_AVX512
|
||||
const Kernels& kernels_intrin_avx512() noexcept;
|
||||
#endif
|
||||
#if JOC_SIMD_HAVE_NEON
|
||||
const Kernels& kernels_intrin_neon() noexcept;
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kKernelCount = static_cast<std::size_t>(Kernel::count);
|
||||
|
||||
// The probe itself lives in cpu_probe.cpp; all this file needs from it is the
|
||||
// cached answer, turned into the two questions the ladder asks: "was it compiled
|
||||
// into this binary" (JOC_SIMD_HAVE_*) and "can this machine run it".
|
||||
|
||||
struct Resolution {
|
||||
Kernels kernels;
|
||||
Isa active[kKernelCount] = {};
|
||||
Isa selected = Isa::scalar;
|
||||
bool forced = false;
|
||||
};
|
||||
|
||||
int isa_rank(Isa isa) noexcept {
|
||||
switch (isa) {
|
||||
case Isa::scalar: return 0;
|
||||
case Isa::sse2: return 1;
|
||||
case Isa::neon: return 1;
|
||||
case Isa::avx2: return 2;
|
||||
case Isa::avx512: return 3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool isa_compiled(Isa isa) noexcept {
|
||||
switch (isa) {
|
||||
case Isa::scalar: return true;
|
||||
// The x86-64 baseline *is* SSE2: the baseline units are compiled for it
|
||||
// and there is no separate unit, because a 128-bit SSE2 register is
|
||||
// exactly the register a scalar double already uses -- SSE2 cannot widen
|
||||
// a double-precision operation, so "sse2" selects the reference kernels.
|
||||
case Isa::sse2: return JOC_SIMD_HAVE_SSE2 != 0;
|
||||
case Isa::avx2: return JOC_SIMD_HAVE_AVX2 != 0;
|
||||
case Isa::avx512: return JOC_SIMD_HAVE_AVX512 != 0;
|
||||
case Isa::neon: return JOC_SIMD_HAVE_NEON != 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const Kernels& kernels_of(Isa isa) noexcept {
|
||||
switch (isa) {
|
||||
case Isa::scalar:
|
||||
case Isa::sse2: return kernels_scalar();
|
||||
#if JOC_SIMD_HAVE_AVX2
|
||||
case Isa::avx2: return kernels_intrin_avx2();
|
||||
#endif
|
||||
#if JOC_SIMD_HAVE_AVX512
|
||||
case Isa::avx512: return kernels_intrin_avx512();
|
||||
#endif
|
||||
#if JOC_SIMD_HAVE_NEON
|
||||
case Isa::neon: return kernels_intrin_neon();
|
||||
#endif
|
||||
default: break;
|
||||
}
|
||||
return kernels_scalar();
|
||||
}
|
||||
|
||||
bool parse_isa(const char* text, Isa* out) noexcept {
|
||||
struct Entry {
|
||||
const char* name;
|
||||
Isa isa;
|
||||
};
|
||||
static const Entry kEntries[] = {
|
||||
{"scalar", Isa::scalar}, {"sse2", Isa::sse2}, {"avx2", Isa::avx2},
|
||||
{"avx512", Isa::avx512}, {"neon", Isa::neon},
|
||||
};
|
||||
for (const Entry& entry : kEntries) {
|
||||
if (std::strcmp(text, entry.name) == 0) {
|
||||
*out = entry.isa;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Resolution resolve() noexcept {
|
||||
Resolution resolution;
|
||||
Isa best = Isa::scalar;
|
||||
bool forced = false;
|
||||
|
||||
if (const char* requested = std::getenv("JOC_SIMD");
|
||||
requested != nullptr && requested[0] != '\0' && std::strcmp(requested, "auto") != 0) {
|
||||
Isa wanted = Isa::scalar;
|
||||
if (!parse_isa(requested, &wanted)) {
|
||||
std::fprintf(stderr,
|
||||
"joc: JOC_SIMD=%s is not one of auto|scalar|sse2|avx2|avx512|neon; "
|
||||
"using auto\n",
|
||||
requested);
|
||||
} else if (!isa_compiled(wanted)) {
|
||||
std::fprintf(stderr, "joc: JOC_SIMD=%s is not part of this build; using auto\n",
|
||||
requested);
|
||||
} else if (!isa_supported(wanted)) {
|
||||
std::fprintf(stderr, "joc: JOC_SIMD=%s is not supported by this CPU or OS; using auto\n",
|
||||
requested);
|
||||
} else {
|
||||
best = wanted;
|
||||
forced = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!forced) {
|
||||
// Widest runnable ISA, in descending order.
|
||||
static const Isa kLadder[] = {Isa::avx512, Isa::avx2, Isa::sse2, Isa::neon};
|
||||
for (const Isa candidate : kLadder) {
|
||||
if (isa_compiled(candidate) && isa_supported(candidate)) {
|
||||
best = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
resolution.selected = best;
|
||||
resolution.forced = forced;
|
||||
|
||||
// Per kernel: widest compiled + runnable implementation at or below the
|
||||
// selected ISA, falling back to the scalar reference, which is always there.
|
||||
static const Isa kCandidates[] = {Isa::avx512, Isa::avx2, Isa::sse2, Isa::neon, Isa::scalar};
|
||||
for (std::size_t index = 0u; index < kKernelCount; ++index) {
|
||||
const Kernel kernel = static_cast<Kernel>(index);
|
||||
resolution.active[index] = Isa::scalar;
|
||||
for (const Isa candidate : kCandidates) {
|
||||
if (isa_rank(candidate) > isa_rank(best) || !isa_compiled(candidate)
|
||||
|| !isa_supported(candidate)) {
|
||||
continue;
|
||||
}
|
||||
const Kernels& set = kernels_of(candidate);
|
||||
bool resolved = false;
|
||||
switch (kernel) {
|
||||
case Kernel::fft_butterflies:
|
||||
if (set.fft_butterflies != nullptr) {
|
||||
resolution.kernels.fft_butterflies = set.fft_butterflies;
|
||||
resolved = true;
|
||||
}
|
||||
break;
|
||||
case Kernel::qmf_synthesis_basis:
|
||||
if (set.qmf_synthesis_basis != nullptr) {
|
||||
resolution.kernels.qmf_synthesis_basis = set.qmf_synthesis_basis;
|
||||
resolved = true;
|
||||
}
|
||||
break;
|
||||
case Kernel::hybrid_low_join:
|
||||
if (set.hybrid_low_join != nullptr) {
|
||||
resolution.kernels.hybrid_low_join = set.hybrid_low_join;
|
||||
resolved = true;
|
||||
}
|
||||
break;
|
||||
case Kernel::render_hybrid_path:
|
||||
if (set.render_hybrid_path != nullptr) {
|
||||
resolution.kernels.render_hybrid_path = set.render_hybrid_path;
|
||||
resolved = true;
|
||||
}
|
||||
break;
|
||||
case Kernel::complex_axpy:
|
||||
if (set.complex_axpy != nullptr) {
|
||||
resolution.kernels.complex_axpy = set.complex_axpy;
|
||||
resolved = true;
|
||||
}
|
||||
break;
|
||||
case Kernel::qmf_analysis_taps:
|
||||
if (set.qmf_analysis_taps != nullptr) {
|
||||
resolution.kernels.qmf_analysis_taps = set.qmf_analysis_taps;
|
||||
resolved = true;
|
||||
}
|
||||
break;
|
||||
case Kernel::complex_multiply:
|
||||
if (set.complex_multiply != nullptr) {
|
||||
resolution.kernels.complex_multiply = set.complex_multiply;
|
||||
resolved = true;
|
||||
}
|
||||
break;
|
||||
case Kernel::count: break;
|
||||
}
|
||||
if (resolved) {
|
||||
resolution.active[index] = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (std::getenv("JOC_SIMD_LOG") != nullptr) {
|
||||
std::fprintf(stderr, "joc: simd selected=%s forced=%d\n", isa_name(resolution.selected),
|
||||
resolution.forced ? 1 : 0);
|
||||
for (std::size_t index = 0u; index < kKernelCount; ++index) {
|
||||
std::fprintf(stderr, "joc: simd kernel[%u]=%s\n", static_cast<unsigned>(index),
|
||||
isa_name(resolution.active[index]));
|
||||
}
|
||||
}
|
||||
return resolution;
|
||||
}
|
||||
|
||||
const Resolution& resolution() noexcept {
|
||||
static const Resolution resolved = resolve();
|
||||
return resolved;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char* isa_name(Isa isa) noexcept {
|
||||
switch (isa) {
|
||||
case Isa::scalar: return "scalar";
|
||||
case Isa::sse2: return "sse2";
|
||||
case Isa::avx2: return "avx2";
|
||||
case Isa::avx512: return "avx512";
|
||||
case Isa::neon: return "neon";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
bool isa_supported(Isa isa) noexcept {
|
||||
const CpuFeatures& cpu = cpu_features();
|
||||
switch (isa) {
|
||||
case Isa::scalar:
|
||||
return true;
|
||||
case Isa::sse2:
|
||||
return cpu.sse2;
|
||||
case Isa::avx2:
|
||||
return cpu.avx2;
|
||||
case Isa::avx512:
|
||||
return cpu.avx512;
|
||||
case Isa::neon:
|
||||
return cpu.neon;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Isa selected_isa() noexcept { return resolution().selected; }
|
||||
|
||||
Isa active_isa(Kernel kernel) noexcept {
|
||||
const std::size_t index = static_cast<std::size_t>(kernel);
|
||||
if (index >= kKernelCount) {
|
||||
return Isa::scalar;
|
||||
}
|
||||
return resolution().active[index];
|
||||
}
|
||||
|
||||
const Kernels& kernels() noexcept { return resolution().kernels; }
|
||||
|
||||
} // namespace joc::simd
|
||||
@@ -0,0 +1,321 @@
|
||||
// AVX2 implementation of the dispatched kernels. Compiled with /arch:AVX2 (or
|
||||
// -mavx2) and only ever called after the dispatcher has confirmed that this CPU
|
||||
// and the OS state it saves can execute AVX2 code.
|
||||
//
|
||||
// CMake gives exactly the `kernels_intrin_*.cpp` units a wider flag, so no
|
||||
// baseline unit can inherit one by accident.
|
||||
//
|
||||
// Nothing in this file may have dynamic initialisation: it is linked into the
|
||||
// same image as the baseline code and would run before the dispatcher.
|
||||
|
||||
#include "simd/simd.h"
|
||||
|
||||
#include <immintrin.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace joc::simd {
|
||||
namespace {
|
||||
|
||||
// Two interleaved complexes per vector.
|
||||
//
|
||||
// The scalar kernel computes, for one butterfly,
|
||||
// odd.re = o.re * w.re - o.im * w.im
|
||||
// odd.im = o.re * w.im + o.im * w.re
|
||||
// with separate multiplies and one rounding per operation. In a 256-bit vector
|
||||
// the even element of every 128-bit half is a real part, so duplicating the even
|
||||
// elements of the twiddle broadcasts w.re onto both components of a complex and
|
||||
// permuting the odd data elements in lines up `o.im`, `o.re` against `w.im`.
|
||||
// addsub then subtracts on the real lanes and adds on the imaginary ones, which
|
||||
// is the same two roundings: `o.im * w.re + o.re * w.im` is the scalar's
|
||||
// `o.re * w.im + o.im * w.re` with the summands exchanged, and floating-point
|
||||
// addition is commutative.
|
||||
inline __m256d mul_pair(__m256d odd, __m256d twiddle) noexcept {
|
||||
const __m256d real = _mm256_mul_pd(odd, _mm256_movedup_pd(twiddle));
|
||||
const __m256d cross = _mm256_mul_pd(_mm256_permute_pd(odd, 0x05), _mm256_permute_pd(twiddle, 0x0f));
|
||||
return _mm256_addsub_pd(real, cross);
|
||||
}
|
||||
|
||||
// The radix-2 DIT cascade over a bit-reversed buffer of any power-of-two size at
|
||||
// or above kMinVectorFftSize.
|
||||
//
|
||||
// Stage s pairs element (start + offset) with (start + offset + 2^s) for every
|
||||
// group of 2^(s+1) elements, and the butterflies of a stage are mutually
|
||||
// independent: none of them reads a slot another one writes. They are therefore
|
||||
// free to share a vector, and each one still accumulates nothing -- every output
|
||||
// is a single add or subtract of two products, in lanes whose operations are
|
||||
// independent of each other.
|
||||
//
|
||||
// Stage 0 is the one case whose group holds a single butterfly, so the pair is
|
||||
// packed from two neighbouring groups instead: their factors are identical
|
||||
// (offset is always 0), and a permute2f128 moves the even and odd halves into
|
||||
// two vector registers.
|
||||
void fft_butterflies_avx2(double* data, std::size_t size, const double* twiddle,
|
||||
const std::size_t* stage_begin) noexcept {
|
||||
{
|
||||
const auto* pair = reinterpret_cast<const __m128d*>(twiddle + 2u * stage_begin[0]);
|
||||
const __m256d factor = _mm256_broadcast_pd(pair);
|
||||
for (std::size_t index = 0u; index < 2u * size; index += 8u) {
|
||||
const __m256d low = _mm256_loadu_pd(data + index);
|
||||
const __m256d high = _mm256_loadu_pd(data + index + 4u);
|
||||
const __m256d even = _mm256_permute2f128_pd(low, high, 0x20);
|
||||
const __m256d odd = _mm256_permute2f128_pd(low, high, 0x31);
|
||||
const __m256d rotated = mul_pair(odd, factor);
|
||||
const __m256d sum = _mm256_add_pd(even, rotated);
|
||||
const __m256d difference = _mm256_sub_pd(even, rotated);
|
||||
_mm256_storeu_pd(data + index, _mm256_permute2f128_pd(sum, difference, 0x20));
|
||||
_mm256_storeu_pd(data + index + 4u, _mm256_permute2f128_pd(sum, difference, 0x31));
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t stage = 1u;
|
||||
for (std::size_t half = 2u; half < size; half <<= 1u, ++stage) {
|
||||
const std::size_t length = half << 1u;
|
||||
const double* table = twiddle + 2u * stage_begin[stage];
|
||||
for (std::size_t start = 0u; start < size; start += length) {
|
||||
for (std::size_t offset = 0u; offset < half; offset += 2u) {
|
||||
const std::size_t even_index = (start + offset) * 2u;
|
||||
const std::size_t odd_index = even_index + half * 2u;
|
||||
const std::size_t factor_index = offset * 2u;
|
||||
const __m256d even = _mm256_loadu_pd(data + even_index);
|
||||
const __m256d odd = _mm256_loadu_pd(data + odd_index);
|
||||
const __m256d factor = _mm256_loadu_pd(table + factor_index);
|
||||
const __m256d rotated = mul_pair(odd, factor);
|
||||
_mm256_storeu_pd(data + even_index, _mm256_add_pd(even, rotated));
|
||||
_mm256_storeu_pd(data + odd_index, _mm256_sub_pd(even, rotated));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// QMF synthesis basis, four ranks of one band per vector.
|
||||
//
|
||||
// The four ranks are four dot products over the same 128 taps, so they are four
|
||||
// independent accumulators and nothing has to be reassociated to fill a vector.
|
||||
// Each lane performs exactly the caller's `sum += values[tap] * weight`, in tap
|
||||
// order, with a separate multiply and add, and the rank-minor table makes the
|
||||
// four weights one contiguous load.
|
||||
//
|
||||
// A band's chain is 128 dependent adds, so the loop is latency-bound long before
|
||||
// it is throughput-bound; the cure is more independent chains, not a shorter
|
||||
// chain (that would reassociate). `Rows` output rows share one weight load and
|
||||
// run their chains side by side -- the caller's rows are consecutive output
|
||||
// channels, which do read the same weights.
|
||||
template <int Rows>
|
||||
void synthesize_rows(const double* values, const double* basis, double* out) noexcept {
|
||||
constexpr std::size_t kRanks = kSynthesisRanks;
|
||||
constexpr std::size_t kTaps = kSynthesisTaps;
|
||||
constexpr std::size_t kRowOut = kSynthesisBands * kRanks;
|
||||
for (std::size_t band = 0u; band < kSynthesisBands; ++band) {
|
||||
const double* table = basis + band * kTaps * kRanks;
|
||||
__m256d sum[Rows];
|
||||
for (int row = 0; row < Rows; ++row) {
|
||||
sum[row] = _mm256_setzero_pd();
|
||||
}
|
||||
for (std::size_t tap = 0u; tap < kTaps; ++tap) {
|
||||
const __m256d weight = _mm256_loadu_pd(table + tap * kRanks);
|
||||
for (int row = 0; row < Rows; ++row) {
|
||||
sum[row] = _mm256_add_pd(
|
||||
sum[row],
|
||||
_mm256_mul_pd(_mm256_broadcast_sd(values + row * kTaps + tap), weight));
|
||||
}
|
||||
}
|
||||
for (int row = 0; row < Rows; ++row) {
|
||||
_mm256_storeu_pd(out + row * kRowOut + band * kRanks, sum[row]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void qmf_synthesis_basis_avx2(const double* values, const double* basis, double* out,
|
||||
std::size_t rows) noexcept {
|
||||
constexpr std::size_t kTaps = kSynthesisTaps;
|
||||
constexpr std::size_t kRowOut = kSynthesisBands * kSynthesisRanks;
|
||||
std::size_t row = 0u;
|
||||
for (; row + 4u <= rows; row += 4u) {
|
||||
synthesize_rows<4>(values + row * kTaps, basis, out + row * kRowOut);
|
||||
}
|
||||
for (; row < rows; ++row) {
|
||||
synthesize_rows<1>(values + row * kTaps, basis, out + row * kRowOut);
|
||||
}
|
||||
}
|
||||
|
||||
// Hybrid analysis low join, one term against all 32 outputs.
|
||||
//
|
||||
// The 32 outputs of a row are independent accumulations over the same 78 terms,
|
||||
// so they are what fills the four vectors: one broadcast of the term's value,
|
||||
// then a multiply and an add per vector. Every lane keeps the term order and the
|
||||
// two roundings of the caller's `out += value * weight`. Terms that are exactly
|
||||
// zero are skipped, which is what the caller does and what leaves its sums
|
||||
// unchanged (a lane's running sum is never a negative zero).
|
||||
void hybrid_low_join_avx2(const double* values, const double* kernel, double* out,
|
||||
std::size_t rows) noexcept {
|
||||
constexpr std::size_t kTerms = kHybridTerms;
|
||||
constexpr std::size_t kOutputs = kHybridOutputs;
|
||||
static_assert(kOutputs == 32u, "eight 256-bit accumulators cover one row's outputs");
|
||||
for (std::size_t row = 0u; row < rows; ++row) {
|
||||
const double* source = values + row * kTerms;
|
||||
double* destination = out + row * kOutputs;
|
||||
__m256d sum0 = _mm256_setzero_pd();
|
||||
__m256d sum1 = _mm256_setzero_pd();
|
||||
__m256d sum2 = _mm256_setzero_pd();
|
||||
__m256d sum3 = _mm256_setzero_pd();
|
||||
__m256d sum4 = _mm256_setzero_pd();
|
||||
__m256d sum5 = _mm256_setzero_pd();
|
||||
__m256d sum6 = _mm256_setzero_pd();
|
||||
__m256d sum7 = _mm256_setzero_pd();
|
||||
for (std::size_t term = 0u; term < kTerms; ++term) {
|
||||
const double value = source[term];
|
||||
if (value == 0.0) {
|
||||
continue;
|
||||
}
|
||||
const double* weights = kernel + term * kOutputs;
|
||||
const __m256d factor = _mm256_broadcast_sd(&value);
|
||||
sum0 = _mm256_add_pd(sum0, _mm256_mul_pd(factor, _mm256_loadu_pd(weights)));
|
||||
sum1 = _mm256_add_pd(sum1, _mm256_mul_pd(factor, _mm256_loadu_pd(weights + 4u)));
|
||||
sum2 = _mm256_add_pd(sum2, _mm256_mul_pd(factor, _mm256_loadu_pd(weights + 8u)));
|
||||
sum3 = _mm256_add_pd(sum3, _mm256_mul_pd(factor, _mm256_loadu_pd(weights + 12u)));
|
||||
sum4 = _mm256_add_pd(sum4, _mm256_mul_pd(factor, _mm256_loadu_pd(weights + 16u)));
|
||||
sum5 = _mm256_add_pd(sum5, _mm256_mul_pd(factor, _mm256_loadu_pd(weights + 20u)));
|
||||
sum6 = _mm256_add_pd(sum6, _mm256_mul_pd(factor, _mm256_loadu_pd(weights + 24u)));
|
||||
sum7 = _mm256_add_pd(sum7, _mm256_mul_pd(factor, _mm256_loadu_pd(weights + 28u)));
|
||||
}
|
||||
_mm256_storeu_pd(destination, sum0);
|
||||
_mm256_storeu_pd(destination + 4u, sum1);
|
||||
_mm256_storeu_pd(destination + 8u, sum2);
|
||||
_mm256_storeu_pd(destination + 12u, sum3);
|
||||
_mm256_storeu_pd(destination + 16u, sum4);
|
||||
_mm256_storeu_pd(destination + 20u, sum5);
|
||||
_mm256_storeu_pd(destination + 24u, sum6);
|
||||
_mm256_storeu_pd(destination + 28u, sum7);
|
||||
}
|
||||
}
|
||||
|
||||
// One rendered path, two bands per vector.
|
||||
//
|
||||
// A band's real and imaginary parts are adjacent, so a vector holds two whole
|
||||
// bands and the cross terms are one swap away -- the same shape the FFT kernel
|
||||
// uses. Two products are needed per component here (`h * t` and `h * swapped(t)`),
|
||||
// each scaled on its own, and the real part is their difference while the
|
||||
// imaginary part is their sum: addsub produces both pairings and a blend keeps
|
||||
// one lane of each. Bands are independent accumulations into independent
|
||||
// outputs, so this is still nothing but cross-band parallelism.
|
||||
void render_hybrid_path_avx2(double* out, const double* transfer, const double* history0,
|
||||
const double* history1, double scale) noexcept {
|
||||
const __m256d factor = _mm256_set1_pd(scale);
|
||||
const double* histories[2] = {history0, history1};
|
||||
for (std::size_t ear = 0u; ear < 2u; ++ear) {
|
||||
const double* source = histories[ear];
|
||||
const double* taps = transfer + ear * kPathBands * 2u;
|
||||
double* destination = out + ear * kPathBands * 2u;
|
||||
std::size_t band = 0u;
|
||||
for (; band + 2u <= kPathBands; band += 2u) {
|
||||
const __m256d history = _mm256_loadu_pd(source + band * 2u);
|
||||
const __m256d tap = _mm256_loadu_pd(taps + band * 2u);
|
||||
const __m256d product = _mm256_mul_pd(_mm256_mul_pd(history, tap), factor);
|
||||
const __m256d crossed = _mm256_mul_pd(
|
||||
_mm256_mul_pd(history, _mm256_permute_pd(tap, 0x05)), factor);
|
||||
const __m256d difference =
|
||||
_mm256_addsub_pd(product, _mm256_permute_pd(product, 0x05));
|
||||
const __m256d sum = _mm256_add_pd(crossed, _mm256_permute_pd(crossed, 0x05));
|
||||
// `difference` carries the real parts in lanes 0 and 2, `sum` the
|
||||
// imaginary ones in lanes 1 and 3 (four elements, so the blend takes a
|
||||
// four-bit selector).
|
||||
const __m256d result = _mm256_blend_pd(difference, sum, 0xa);
|
||||
_mm256_storeu_pd(destination + band * 2u,
|
||||
_mm256_add_pd(_mm256_loadu_pd(destination + band * 2u), result));
|
||||
}
|
||||
for (; band < kPathBands; ++band) {
|
||||
const double source_real = source[band * 2u];
|
||||
const double source_imag = source[band * 2u + 1u];
|
||||
const double tap_real = taps[band * 2u];
|
||||
const double tap_imag = taps[band * 2u + 1u];
|
||||
destination[band * 2u] +=
|
||||
source_real * tap_real * scale - source_imag * tap_imag * scale;
|
||||
destination[band * 2u + 1u] +=
|
||||
source_real * tap_imag * scale + source_imag * tap_real * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar times a run of interleaved complexes, accumulated in place. The elements
|
||||
// are independent, so two complexes (one 256-bit vector) go together and each
|
||||
// component keeps the caller's own multiply-then-add.
|
||||
void complex_axpy_avx2(double* out, const double* field, double scale,
|
||||
std::size_t count) noexcept {
|
||||
const __m256d factor = _mm256_set1_pd(scale);
|
||||
std::size_t index = 0u;
|
||||
for (; index + 2u <= count; index += 2u) {
|
||||
const std::size_t offset = index * 2u;
|
||||
_mm256_storeu_pd(out + offset,
|
||||
_mm256_add_pd(_mm256_loadu_pd(out + offset),
|
||||
_mm256_mul_pd(_mm256_loadu_pd(field + offset), factor)));
|
||||
}
|
||||
for (; index < count; ++index) {
|
||||
out[index * 2u] += field[index * 2u] * scale;
|
||||
out[index * 2u + 1u] += field[index * 2u + 1u] * scale;
|
||||
}
|
||||
}
|
||||
|
||||
// QMF analysis polyphase accumulate: the 64 bands of a row are 64 independent
|
||||
// accumulations of one product each, and the coefficient row is reused by every
|
||||
// row, so the rows stream past a first-level-cache-resident set of coefficients.
|
||||
void qmf_analysis_taps_avx2(double* out, const double* source, const double* coefficients,
|
||||
std::size_t rows) noexcept {
|
||||
for (std::size_t row = 0u; row < rows; ++row) {
|
||||
double* destination = out + row * kQmfAnalysisBands;
|
||||
const double* values = source + row * kQmfAnalysisBands;
|
||||
for (std::size_t band = 0u; band < kQmfAnalysisBands; band += 4u) {
|
||||
_mm256_storeu_pd(destination + band,
|
||||
_mm256_add_pd(_mm256_loadu_pd(destination + band),
|
||||
_mm256_mul_pd(_mm256_loadu_pd(values + band),
|
||||
_mm256_loadu_pd(coefficients + band))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Element-wise complex product of two runs, two complexes per vector. Both
|
||||
// operands take part in every lane, so both products of the scalar formula are
|
||||
// formed and addsub pairs the real difference with the imaginary sum: the same
|
||||
// four multiplies and the same two roundings per component as `a * b`.
|
||||
void complex_multiply_avx2(double* out, const double* left, const double* right,
|
||||
std::size_t count) noexcept {
|
||||
std::size_t index = 0u;
|
||||
for (; index + 2u <= count; index += 2u) {
|
||||
const std::size_t offset = index * 2u;
|
||||
const __m256d first = _mm256_loadu_pd(left + offset);
|
||||
const __m256d second = _mm256_loadu_pd(right + offset);
|
||||
// (re*re, im*im) and (im*re, re*im): the first gives the real difference,
|
||||
// the second the imaginary sum.
|
||||
const __m256d product = _mm256_mul_pd(first, second);
|
||||
const __m256d crossed = _mm256_mul_pd(_mm256_permute_pd(first, 0x05), second);
|
||||
const __m256d difference =
|
||||
_mm256_addsub_pd(product, _mm256_permute_pd(product, 0x05));
|
||||
const __m256d total = _mm256_add_pd(crossed, _mm256_permute_pd(crossed, 0x05));
|
||||
_mm256_storeu_pd(out + offset, _mm256_blend_pd(difference, total, 0xa));
|
||||
}
|
||||
for (; index < count; ++index) {
|
||||
const std::size_t offset = index * 2u;
|
||||
const double left_real = left[offset];
|
||||
const double left_imag = left[offset + 1u];
|
||||
const double right_real = right[offset];
|
||||
const double right_imag = right[offset + 1u];
|
||||
out[offset] = left_real * right_real - left_imag * right_imag;
|
||||
out[offset + 1u] = left_real * right_imag + left_imag * right_real;
|
||||
}
|
||||
}
|
||||
|
||||
const Kernels kAvx2{
|
||||
&fft_butterflies_avx2,
|
||||
&qmf_synthesis_basis_avx2,
|
||||
&hybrid_low_join_avx2,
|
||||
&render_hybrid_path_avx2,
|
||||
&complex_axpy_avx2,
|
||||
&qmf_analysis_taps_avx2,
|
||||
&complex_multiply_avx2,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
const Kernels& kernels_intrin_avx2() noexcept { return kAvx2; }
|
||||
|
||||
} // namespace joc::simd
|
||||
@@ -0,0 +1,272 @@
|
||||
// AVX-512F implementation of the dispatched kernels. Compiled with /arch:AVX512
|
||||
// (or -mavx512f) and only ever called after the dispatcher has confirmed AVX-512F
|
||||
// *and* that the OS saves the opmask/ZMM state.
|
||||
//
|
||||
// CMake gives exactly the `kernels_intrin_*.cpp` units a wider flag, so no
|
||||
// baseline unit can inherit one by accident.
|
||||
//
|
||||
// Nothing in this file may have dynamic initialisation: it is linked into the
|
||||
// same image as the baseline code and would run before the dispatcher.
|
||||
|
||||
#include "simd/simd.h"
|
||||
|
||||
#include <immintrin.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace joc::simd {
|
||||
namespace {
|
||||
|
||||
// Four interleaved complexes per vector: a 512-bit register is four 128-bit
|
||||
// lanes, each holding one (re, im) pair, so the AVX2 argument applies one lane
|
||||
// wider -- movedup/permute broadcast the twiddle components inside every lane.
|
||||
// AVX-512 never widened VADDSUBPD to 512 bits, so the real lanes take the
|
||||
// difference and the imaginary ones the sum through merge masking: that is still
|
||||
// one exact subtract and one exact add per output, with the same two roundings
|
||||
// per component and no reassociation.
|
||||
inline __m512d mul_quad(__m512d odd, __m512d twiddle) noexcept {
|
||||
const __m512d real = _mm512_mul_pd(odd, _mm512_movedup_pd(twiddle));
|
||||
const __m512d cross =
|
||||
_mm512_mul_pd(_mm512_permute_pd(odd, 0x55), _mm512_permute_pd(twiddle, 0xff));
|
||||
const __m512d upper = _mm512_mask_add_pd(real, 0xaa, real, cross);
|
||||
return _mm512_mask_sub_pd(upper, 0x55, real, cross);
|
||||
}
|
||||
|
||||
// Stages 0 and 1 hold fewer than four complexes in a group, so their butterflies
|
||||
// are packed from neighbouring groups instead; the factor depends only on the
|
||||
// offset inside a group, so one broadcast serves every packed lane.
|
||||
//
|
||||
// VSHUFF64X2 takes a two-bit selector per output lane, but its first two output
|
||||
// lanes can only name lanes of its first operand and its last two only lanes of
|
||||
// its second (verified against the instruction). These are the immediates the
|
||||
// packings need, written as the four lane selections they spell out.
|
||||
constexpr int kHalvesEven = 0x88; // (low.l0, low.l2, high.l0, high.l2) = x0,x2,x4,x6
|
||||
constexpr int kHalvesOdd = 0xdd; // (low.l1, low.l3, high.l1, high.l3) = x1,x3,x5,x7
|
||||
constexpr int kGroupsEven = 0x44; // (low.l0, low.l1, high.l0, high.l1)
|
||||
constexpr int kGroupsOdd = 0xee; // (low.l2, low.l3, high.l2, high.l3)
|
||||
constexpr int kSwapMiddle = 0xd8; // (a.l0, a.l2, a.l1, a.l3)
|
||||
|
||||
void pack_halves(double* buffer, __m512d factor) noexcept {
|
||||
const __m512d low = _mm512_loadu_pd(buffer);
|
||||
const __m512d high = _mm512_loadu_pd(buffer + 8u);
|
||||
const __m512d even = _mm512_shuffle_f64x2(low, high, kHalvesEven);
|
||||
const __m512d odd = _mm512_shuffle_f64x2(low, high, kHalvesOdd);
|
||||
const __m512d rotated = mul_quad(odd, factor);
|
||||
const __m512d sum = _mm512_add_pd(even, rotated);
|
||||
const __m512d difference = _mm512_sub_pd(even, rotated);
|
||||
// `sum` holds the results for the even positions of the four packed groups and
|
||||
// `difference` those for the odd ones, and the buffer wants them interleaved
|
||||
// again. One shuffle cannot do that, so each store pairs the two halves and
|
||||
// then swaps the two middle lanes of the pair.
|
||||
const __m512d pairs_low = _mm512_shuffle_f64x2(sum, difference, kGroupsEven);
|
||||
const __m512d pairs_high = _mm512_shuffle_f64x2(sum, difference, kGroupsOdd);
|
||||
_mm512_storeu_pd(buffer, _mm512_shuffle_f64x2(pairs_low, pairs_low, kSwapMiddle));
|
||||
_mm512_storeu_pd(buffer + 8u, _mm512_shuffle_f64x2(pairs_high, pairs_high, kSwapMiddle));
|
||||
}
|
||||
|
||||
void pack_groups(double* buffer, __m512d factor) noexcept {
|
||||
const __m512d low = _mm512_loadu_pd(buffer);
|
||||
const __m512d high = _mm512_loadu_pd(buffer + 8u);
|
||||
const __m512d even = _mm512_shuffle_f64x2(low, high, kGroupsEven);
|
||||
const __m512d odd = _mm512_shuffle_f64x2(low, high, kGroupsOdd);
|
||||
const __m512d rotated = mul_quad(odd, factor);
|
||||
const __m512d sum = _mm512_add_pd(even, rotated);
|
||||
const __m512d difference = _mm512_sub_pd(even, rotated);
|
||||
_mm512_storeu_pd(buffer, _mm512_shuffle_f64x2(sum, difference, kGroupsEven));
|
||||
_mm512_storeu_pd(buffer + 8u, _mm512_shuffle_f64x2(sum, difference, kGroupsOdd));
|
||||
}
|
||||
|
||||
void fft_butterflies_avx512(double* buffer, std::size_t size, const double* twiddle,
|
||||
const std::size_t* stage_begin) noexcept {
|
||||
// Stage 0: length 2, one butterfly per group, one factor for all of them.
|
||||
{
|
||||
const __m128d pair = _mm_loadu_pd(twiddle + 2u * stage_begin[0]);
|
||||
const __m512d factor = _mm512_broadcast_f64x4(_mm256_broadcast_pd(&pair));
|
||||
for (std::size_t index = 0u; index < 2u * size; index += 16u) {
|
||||
pack_halves(buffer + index, factor);
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 1: length 4, two butterflies per group and two factors.
|
||||
{
|
||||
const __m512d factor =
|
||||
_mm512_broadcast_f64x4(_mm256_loadu_pd(twiddle + 2u * stage_begin[1]));
|
||||
for (std::size_t index = 0u; index < 2u * size; index += 16u) {
|
||||
pack_groups(buffer + index, factor);
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t stage = 2u;
|
||||
for (std::size_t half = 4u; half < size; half <<= 1u, ++stage) {
|
||||
const std::size_t length = half << 1u;
|
||||
const double* table = twiddle + 2u * stage_begin[stage];
|
||||
for (std::size_t start = 0u; start < size; start += length) {
|
||||
for (std::size_t offset = 0u; offset < half; offset += 4u) {
|
||||
const std::size_t even_index = (start + offset) * 2u;
|
||||
const std::size_t odd_index = even_index + half * 2u;
|
||||
const std::size_t factor_index = offset * 2u;
|
||||
const __m512d even = _mm512_loadu_pd(buffer + even_index);
|
||||
const __m512d odd = _mm512_loadu_pd(buffer + odd_index);
|
||||
const __m512d factor = _mm512_loadu_pd(table + factor_index);
|
||||
const __m512d rotated = mul_quad(odd, factor);
|
||||
_mm512_storeu_pd(buffer + even_index, _mm512_add_pd(even, rotated));
|
||||
_mm512_storeu_pd(buffer + odd_index, _mm512_sub_pd(even, rotated));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar times a run of interleaved complexes: four complexes (eight doubles) per
|
||||
// vector, every element an independent accumulation.
|
||||
void complex_axpy_avx512(double* out, const double* field, double scale,
|
||||
std::size_t count) noexcept {
|
||||
const __m512d factor = _mm512_set1_pd(scale);
|
||||
std::size_t index = 0u;
|
||||
for (; index + 4u <= count; index += 4u) {
|
||||
const std::size_t offset = index * 2u;
|
||||
_mm512_storeu_pd(out + offset,
|
||||
_mm512_add_pd(_mm512_loadu_pd(out + offset),
|
||||
_mm512_mul_pd(_mm512_loadu_pd(field + offset), factor)));
|
||||
}
|
||||
for (; index < count; ++index) {
|
||||
out[index * 2u] += field[index * 2u] * scale;
|
||||
out[index * 2u + 1u] += field[index * 2u + 1u] * scale;
|
||||
}
|
||||
}
|
||||
|
||||
// QMF analysis polyphase accumulate, eight bands per vector.
|
||||
void qmf_analysis_taps_avx512(double* out, const double* source, const double* coefficients,
|
||||
std::size_t rows) noexcept {
|
||||
for (std::size_t row = 0u; row < rows; ++row) {
|
||||
double* destination = out + row * kQmfAnalysisBands;
|
||||
const double* values = source + row * kQmfAnalysisBands;
|
||||
for (std::size_t band = 0u; band < kQmfAnalysisBands; band += 8u) {
|
||||
_mm512_storeu_pd(destination + band,
|
||||
_mm512_add_pd(_mm512_loadu_pd(destination + band),
|
||||
_mm512_mul_pd(_mm512_loadu_pd(values + band),
|
||||
_mm512_loadu_pd(coefficients + band))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hybrid analysis low join: 32 outputs are exactly four 512-bit vectors, so one
|
||||
// broadcast of the term's value feeds four multiplies and four adds.
|
||||
void hybrid_low_join_avx512(const double* values, const double* kernel, double* out,
|
||||
std::size_t rows) noexcept {
|
||||
constexpr std::size_t kTerms = kHybridTerms;
|
||||
constexpr std::size_t kOutputs = kHybridOutputs;
|
||||
static_assert(kOutputs == 32u, "four 512-bit accumulators cover one row");
|
||||
for (std::size_t row = 0u; row < rows; ++row) {
|
||||
const double* source = values + row * kTerms;
|
||||
double* destination = out + row * kOutputs;
|
||||
__m512d sum0 = _mm512_setzero_pd();
|
||||
__m512d sum1 = _mm512_setzero_pd();
|
||||
__m512d sum2 = _mm512_setzero_pd();
|
||||
__m512d sum3 = _mm512_setzero_pd();
|
||||
for (std::size_t term = 0u; term < kTerms; ++term) {
|
||||
const double value = source[term];
|
||||
if (value == 0.0) {
|
||||
continue;
|
||||
}
|
||||
const double* weights = kernel + term * kOutputs;
|
||||
const __m512d factor = _mm512_set1_pd(value);
|
||||
sum0 = _mm512_add_pd(sum0, _mm512_mul_pd(factor, _mm512_loadu_pd(weights)));
|
||||
sum1 = _mm512_add_pd(sum1, _mm512_mul_pd(factor, _mm512_loadu_pd(weights + 8u)));
|
||||
sum2 = _mm512_add_pd(sum2, _mm512_mul_pd(factor, _mm512_loadu_pd(weights + 16u)));
|
||||
sum3 = _mm512_add_pd(sum3, _mm512_mul_pd(factor, _mm512_loadu_pd(weights + 24u)));
|
||||
}
|
||||
_mm512_storeu_pd(destination, sum0);
|
||||
_mm512_storeu_pd(destination + 8u, sum1);
|
||||
_mm512_storeu_pd(destination + 16u, sum2);
|
||||
_mm512_storeu_pd(destination + 24u, sum3);
|
||||
}
|
||||
}
|
||||
|
||||
// One rendered path, four bands per vector. AVX-512 has no 512-bit addsub, so the
|
||||
// two pairings are built with merge masking -- still one exact subtract and one
|
||||
// exact add per output, and the same separate scale multiply per product.
|
||||
void render_hybrid_path_avx512(double* out, const double* transfer, const double* history0,
|
||||
const double* history1, double scale) noexcept {
|
||||
const __m512d factor = _mm512_set1_pd(scale);
|
||||
const double* histories[2] = {history0, history1};
|
||||
for (std::size_t ear = 0u; ear < 2u; ++ear) {
|
||||
const double* source = histories[ear];
|
||||
const double* taps = transfer + ear * kPathBands * 2u;
|
||||
double* destination = out + ear * kPathBands * 2u;
|
||||
std::size_t band = 0u;
|
||||
for (; band + 4u <= kPathBands; band += 4u) {
|
||||
const __m512d history = _mm512_loadu_pd(source + band * 2u);
|
||||
const __m512d tap = _mm512_loadu_pd(taps + band * 2u);
|
||||
const __m512d product = _mm512_mul_pd(
|
||||
_mm512_mul_pd(history, tap), factor);
|
||||
const __m512d crossed = _mm512_mul_pd(
|
||||
_mm512_mul_pd(history, _mm512_permute_pd(tap, 0x55)), factor);
|
||||
const __m512d swapped_product = _mm512_permute_pd(product, 0x55);
|
||||
const __m512d swapped_crossed = _mm512_permute_pd(crossed, 0x55);
|
||||
// The real component is the difference of the two products and sits in
|
||||
// the even element of each complex, the imaginary one is the sum of the
|
||||
// crossed pair and sits in the odd element -- so the subtract is masked
|
||||
// to the even lanes and the blend keeps the sum on the odd ones.
|
||||
const __m512d difference =
|
||||
_mm512_mask_sub_pd(product, 0x55, product, swapped_product);
|
||||
const __m512d total = _mm512_add_pd(crossed, swapped_crossed);
|
||||
const __m512d result = _mm512_mask_blend_pd(0xaa, difference, total);
|
||||
_mm512_storeu_pd(destination + band * 2u,
|
||||
_mm512_add_pd(_mm512_loadu_pd(destination + band * 2u), result));
|
||||
}
|
||||
for (; band < kPathBands; ++band) {
|
||||
const double source_real = source[band * 2u];
|
||||
const double source_imag = source[band * 2u + 1u];
|
||||
const double tap_real = taps[band * 2u];
|
||||
const double tap_imag = taps[band * 2u + 1u];
|
||||
destination[band * 2u] +=
|
||||
source_real * tap_real * scale - source_imag * tap_imag * scale;
|
||||
destination[band * 2u + 1u] +=
|
||||
source_real * tap_imag * scale + source_imag * tap_real * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Element-wise complex product, four complexes per vector. AVX-512 has no 512-bit
|
||||
// addsub, so the real difference is masked into the even lanes and the imaginary
|
||||
// sum into the odd ones.
|
||||
void complex_multiply_avx512(double* out, const double* left, const double* right,
|
||||
std::size_t count) noexcept {
|
||||
std::size_t index = 0u;
|
||||
for (; index + 4u <= count; index += 4u) {
|
||||
const std::size_t offset = index * 2u;
|
||||
const __m512d first = _mm512_loadu_pd(left + offset);
|
||||
const __m512d second = _mm512_loadu_pd(right + offset);
|
||||
const __m512d product = _mm512_mul_pd(first, second);
|
||||
const __m512d crossed = _mm512_mul_pd(_mm512_permute_pd(first, 0x55), second);
|
||||
const __m512d difference =
|
||||
_mm512_mask_sub_pd(product, 0x55, product, _mm512_permute_pd(product, 0x55));
|
||||
const __m512d total = _mm512_add_pd(crossed, _mm512_permute_pd(crossed, 0x55));
|
||||
_mm512_storeu_pd(out + offset, _mm512_mask_blend_pd(0xaa, difference, total));
|
||||
}
|
||||
for (; index < count; ++index) {
|
||||
const std::size_t offset = index * 2u;
|
||||
const double left_real = left[offset];
|
||||
const double left_imag = left[offset + 1u];
|
||||
const double right_real = right[offset];
|
||||
const double right_imag = right[offset + 1u];
|
||||
out[offset] = left_real * right_real - left_imag * right_imag;
|
||||
out[offset + 1u] = left_real * right_imag + left_imag * right_real;
|
||||
}
|
||||
}
|
||||
|
||||
const Kernels kAvx512{
|
||||
&fft_butterflies_avx512,
|
||||
nullptr, // qmf_synthesis_basis: four ranks do not fill a 512-bit vector
|
||||
&hybrid_low_join_avx512,
|
||||
&render_hybrid_path_avx512,
|
||||
&complex_axpy_avx512,
|
||||
&qmf_analysis_taps_avx512,
|
||||
&complex_multiply_avx512,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
const Kernels& kernels_intrin_avx512() noexcept { return kAvx512; }
|
||||
|
||||
} // namespace joc::simd
|
||||
@@ -0,0 +1,311 @@
|
||||
// AArch64 NEON (ASIMD) implementation of the dispatched kernels.
|
||||
//
|
||||
// CMake gives exactly the `kernels_intrin_*.cpp` units their ISA flag, so no
|
||||
// baseline unit can inherit one by accident.
|
||||
//
|
||||
// Unlike the x86 units this one needs no feature probe -- ASIMD is architectural
|
||||
// for AArch64 -- but it is still only reached through the dispatcher, so
|
||||
// JOC_SIMD=scalar, sse2 or neon all stay available on the same binary.
|
||||
//
|
||||
// A NEON register holds two doubles, so a vector carries two lanes where an AVX2
|
||||
// vector carries four. The lanes are the same independent work items as in the
|
||||
// AVX2 unit, and each lane performs exactly the scalar reference's operations in
|
||||
// the scalar reference's order: separate multiplies and adds, never a fused
|
||||
// multiply-add, and never a reassociated sum.
|
||||
//
|
||||
// Nothing in this file may have dynamic initialisation (it is linked into the
|
||||
// same image as the baseline code and would run before the dispatcher).
|
||||
//
|
||||
// NOTE: the unit tests do not exercise this unit. It is built by the AArch64 CI
|
||||
// job, and its bit-exactness is argued from the shared kernel contract (simd.h)
|
||||
// rather than measured here.
|
||||
|
||||
#include "simd/simd.h"
|
||||
|
||||
#include <arm_neon.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "foundation/fft.h"
|
||||
|
||||
namespace joc::simd {
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kFftSize = dsp::kQmfFftSize;
|
||||
static_assert(kFftSize == 128u, "the QMF transform is the 128-point case");
|
||||
|
||||
// ------------------------------------------------------------ radix-2 FFT ----
|
||||
//
|
||||
// Two butterflies per iteration, held as separate real/imaginary vectors: vld2q
|
||||
// loads two interleaved complexes and deinterleaves them, vst2q puts them back.
|
||||
// `sum = even + odd * w` and `difference = even - odd * w` are then two add/sub
|
||||
// pairs, and the complex product is four multiplies and two add/subs, which is
|
||||
// what the scalar kernel writes.
|
||||
inline float64x2x2_t complex_mul(float64x2_t odd_real, float64x2_t odd_imag,
|
||||
float64x2_t weight_real, float64x2_t weight_imag) {
|
||||
float64x2x2_t result;
|
||||
result.val[0] = vsubq_f64(vmulq_f64(odd_real, weight_real), vmulq_f64(odd_imag, weight_imag));
|
||||
result.val[1] = vaddq_f64(vmulq_f64(odd_real, weight_imag), vmulq_f64(odd_imag, weight_real));
|
||||
return result;
|
||||
}
|
||||
|
||||
// Stages 0 and 1 hold fewer than two complexes per group, so their butterflies are
|
||||
// packed from neighbouring groups; the factor depends only on the offset inside a
|
||||
// group, so the two lanes share it.
|
||||
void fft_butterflies_neon(double* buffer, std::size_t size, const double* twiddle,
|
||||
const std::size_t* stage_begin) noexcept {
|
||||
// Stage 0: length 2, one butterfly per group. The two lanes take the two
|
||||
// halves of two neighbouring groups, which the transposes select.
|
||||
{
|
||||
const double* table = twiddle + 2u * stage_begin[0];
|
||||
const float64x2_t weight_real = vdupq_n_f64(table[0]);
|
||||
const float64x2_t weight_imag = vdupq_n_f64(table[1]);
|
||||
for (std::size_t index = 0u; index < 2u * size; index += 8u) {
|
||||
const float64x2_t first = vld1q_f64(buffer + index);
|
||||
const float64x2_t second = vld1q_f64(buffer + index + 2u);
|
||||
const float64x2_t third = vld1q_f64(buffer + index + 4u);
|
||||
const float64x2_t fourth = vld1q_f64(buffer + index + 6u);
|
||||
const float64x2_t even_real = vtrn1q_f64(first, third);
|
||||
const float64x2_t even_imag = vtrn2q_f64(first, third);
|
||||
const float64x2_t odd_real = vtrn1q_f64(second, fourth);
|
||||
const float64x2_t odd_imag = vtrn2q_f64(second, fourth);
|
||||
const float64x2x2_t rotated =
|
||||
complex_mul(odd_real, odd_imag, weight_real, weight_imag);
|
||||
const float64x2_t sum_real = vaddq_f64(even_real, rotated.val[0]);
|
||||
const float64x2_t sum_imag = vaddq_f64(even_imag, rotated.val[1]);
|
||||
const float64x2_t difference_real = vsubq_f64(even_real, rotated.val[0]);
|
||||
const float64x2_t difference_imag = vsubq_f64(even_imag, rotated.val[1]);
|
||||
// sum.* are the two groups' even-position results and difference.* their
|
||||
// odd-position ones; a transpose of the two component vectors puts each
|
||||
// complex back together, in the order the groups appear.
|
||||
vst1q_f64(buffer + index, vtrn1q_f64(sum_real, sum_imag));
|
||||
vst1q_f64(buffer + index + 2u, vtrn1q_f64(difference_real, difference_imag));
|
||||
vst1q_f64(buffer + index + 4u, vtrn2q_f64(sum_real, sum_imag));
|
||||
vst1q_f64(buffer + index + 6u, vtrn2q_f64(difference_real, difference_imag));
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t stage = 1u;
|
||||
for (std::size_t half = 2u; half < size; half <<= 1u, ++stage) {
|
||||
const std::size_t length = half << 1u;
|
||||
const double* table = twiddle + 2u * stage_begin[stage];
|
||||
for (std::size_t start = 0u; start < size; start += length) {
|
||||
for (std::size_t offset = 0u; offset < half; offset += 2u) {
|
||||
double* even_values = buffer + (start + offset) * 2u;
|
||||
double* odd_values = even_values + half * 2u;
|
||||
const float64x2x2_t even = vld2q_f64(even_values);
|
||||
const float64x2x2_t odd = vld2q_f64(odd_values);
|
||||
const float64x2x2_t weight = vld2q_f64(table + offset * 2u);
|
||||
const float64x2x2_t rotated =
|
||||
complex_mul(odd.val[0], odd.val[1], weight.val[0], weight.val[1]);
|
||||
float64x2x2_t sum;
|
||||
float64x2x2_t difference;
|
||||
sum.val[0] = vaddq_f64(even.val[0], rotated.val[0]);
|
||||
sum.val[1] = vaddq_f64(even.val[1], rotated.val[1]);
|
||||
difference.val[0] = vsubq_f64(even.val[0], rotated.val[0]);
|
||||
difference.val[1] = vsubq_f64(even.val[1], rotated.val[1]);
|
||||
vst2q_f64(even_values, sum);
|
||||
vst2q_f64(odd_values, difference);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------- QMF synthesis basis -----
|
||||
//
|
||||
// The four ranks of a band are four independent dot products over the same row, so
|
||||
// two registers carry four lanes. `Rows` rows run side by side because a band's
|
||||
// chain is 128 dependent adds: the cure is more independent chains, not a shorter
|
||||
// chain.
|
||||
template <int Rows>
|
||||
void synthesize_rows_neon(const double* values, const double* basis, double* out) noexcept {
|
||||
constexpr std::size_t kRanks = kSynthesisRanks;
|
||||
constexpr std::size_t kTaps = kSynthesisTaps;
|
||||
constexpr std::size_t kRowOut = kSynthesisBands * kRanks;
|
||||
for (std::size_t band = 0u; band < kSynthesisBands; ++band) {
|
||||
const double* table = basis + band * kTaps * kRanks;
|
||||
float64x2_t low[Rows];
|
||||
float64x2_t high[Rows];
|
||||
for (int row = 0; row < Rows; ++row) {
|
||||
low[row] = vdupq_n_f64(0.0);
|
||||
high[row] = vdupq_n_f64(0.0);
|
||||
}
|
||||
for (std::size_t tap = 0u; tap < kTaps; ++tap) {
|
||||
const float64x2_t weight_low = vld1q_f64(table + tap * kRanks);
|
||||
const float64x2_t weight_high = vld1q_f64(table + tap * kRanks + 2u);
|
||||
for (int row = 0; row < Rows; ++row) {
|
||||
const float64x2_t factor = vdupq_n_f64(values[row * kTaps + tap]);
|
||||
low[row] = vaddq_f64(low[row], vmulq_f64(factor, weight_low));
|
||||
high[row] = vaddq_f64(high[row], vmulq_f64(factor, weight_high));
|
||||
}
|
||||
}
|
||||
for (int row = 0; row < Rows; ++row) {
|
||||
vst1q_f64(out + row * kRowOut + band * kRanks, low[row]);
|
||||
vst1q_f64(out + row * kRowOut + band * kRanks + 2u, high[row]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void qmf_synthesis_basis_neon(const double* values, const double* basis, double* out,
|
||||
std::size_t rows) noexcept {
|
||||
constexpr std::size_t kTaps = kSynthesisTaps;
|
||||
constexpr std::size_t kRowOut = kSynthesisBands * kSynthesisRanks;
|
||||
std::size_t row = 0u;
|
||||
for (; row + 4u <= rows; row += 4u) {
|
||||
synthesize_rows_neon<4>(values + row * kTaps, basis, out + row * kRowOut);
|
||||
}
|
||||
for (; row < rows; ++row) {
|
||||
synthesize_rows_neon<1>(values + row * kTaps, basis, out + row * kRowOut);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------ Hybrid low join -----
|
||||
//
|
||||
// One term against all 32 outputs, two outputs per register: the outputs are
|
||||
// independent accumulations over the same terms, which is what the lanes carry.
|
||||
void hybrid_low_join_neon(const double* values, const double* kernel, double* out,
|
||||
std::size_t rows) noexcept {
|
||||
constexpr std::size_t kTerms = kHybridTerms;
|
||||
constexpr std::size_t kOutputs = kHybridOutputs;
|
||||
static_assert(kOutputs == 32u, "sixteen 128-bit accumulators cover one row");
|
||||
for (std::size_t row = 0u; row < rows; ++row) {
|
||||
const double* source = values + row * kTerms;
|
||||
double* destination = out + row * kOutputs;
|
||||
float64x2_t partial[16];
|
||||
for (int chunk = 0; chunk < 16; ++chunk) {
|
||||
partial[chunk] = vdupq_n_f64(0.0);
|
||||
}
|
||||
for (std::size_t term = 0u; term < kTerms; ++term) {
|
||||
const double value = source[term];
|
||||
if (value == 0.0) {
|
||||
continue;
|
||||
}
|
||||
const double* weights = kernel + term * kOutputs;
|
||||
const float64x2_t factor = vdupq_n_f64(value);
|
||||
for (int chunk = 0; chunk < 16; ++chunk) {
|
||||
partial[chunk] = vaddq_f64(
|
||||
partial[chunk], vmulq_f64(factor, vld1q_f64(weights + chunk * 2)));
|
||||
}
|
||||
}
|
||||
for (int chunk = 0; chunk < 16; ++chunk) {
|
||||
vst1q_f64(destination + chunk * 2, partial[chunk]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------- Rendered hybrid path ---
|
||||
//
|
||||
// Two bands per iteration, real and imaginary held apart and reloaded from memory
|
||||
// in the caller's own layout: `out += h * t * scale` for the real part and
|
||||
// `out += h * swapped(t) * scale` for the imaginary one, with the scale applied to
|
||||
// each product separately.
|
||||
void render_hybrid_path_neon(double* out, const double* transfer, const double* history0,
|
||||
const double* history1, double scale) noexcept {
|
||||
const float64x2_t factor = vdupq_n_f64(scale);
|
||||
const double* histories[2] = {history0, history1};
|
||||
for (std::size_t ear = 0u; ear < 2u; ++ear) {
|
||||
const double* source = histories[ear];
|
||||
const double* taps = transfer + ear * kPathBands * 2u;
|
||||
double* destination = out + ear * kPathBands * 2u;
|
||||
std::size_t band = 0u;
|
||||
for (; band + 2u <= kPathBands; band += 2u) {
|
||||
const float64x2x2_t history = vld2q_f64(source + band * 2u);
|
||||
const float64x2x2_t tap = vld2q_f64(taps + band * 2u);
|
||||
float64x2x2_t output = vld2q_f64(destination + band * 2u);
|
||||
// The caller scales each product on its own before combining, so the
|
||||
// scale is a separate multiply of every product -- not a multiply of the
|
||||
// difference and the sum, which would round differently.
|
||||
const float64x2_t real = vsubq_f64(
|
||||
vmulq_f64(vmulq_f64(history.val[0], tap.val[0]), factor),
|
||||
vmulq_f64(vmulq_f64(history.val[1], tap.val[1]), factor));
|
||||
const float64x2_t imaginary = vaddq_f64(
|
||||
vmulq_f64(vmulq_f64(history.val[0], tap.val[1]), factor),
|
||||
vmulq_f64(vmulq_f64(history.val[1], tap.val[0]), factor));
|
||||
output.val[0] = vaddq_f64(output.val[0], real);
|
||||
output.val[1] = vaddq_f64(output.val[1], imaginary);
|
||||
vst2q_f64(destination + band * 2u, output);
|
||||
}
|
||||
for (; band < kPathBands; ++band) {
|
||||
const double source_real = source[band * 2u];
|
||||
const double source_imag = source[band * 2u + 1u];
|
||||
const double tap_real = taps[band * 2u];
|
||||
const double tap_imag = taps[band * 2u + 1u];
|
||||
destination[band * 2u] +=
|
||||
source_real * tap_real * scale - source_imag * tap_imag * scale;
|
||||
destination[band * 2u + 1u] +=
|
||||
source_real * tap_imag * scale + source_imag * tap_real * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar times a run of interleaved complexes, two complexes per iteration.
|
||||
void complex_axpy_neon(double* out, const double* field, double scale,
|
||||
std::size_t count) noexcept {
|
||||
const float64x2_t factor = vdupq_n_f64(scale);
|
||||
std::size_t index = 0u;
|
||||
for (; index + 2u <= count; index += 2u) {
|
||||
const float64x2_t values = vld1q_f64(field + index * 2u);
|
||||
const float64x2_t accumulated = vld1q_f64(out + index * 2u);
|
||||
vst1q_f64(out + index * 2u, vaddq_f64(accumulated, vmulq_f64(values, factor)));
|
||||
}
|
||||
for (; index < count; ++index) {
|
||||
out[index * 2u] += field[index * 2u] * scale;
|
||||
out[index * 2u + 1u] += field[index * 2u + 1u] * scale;
|
||||
}
|
||||
}
|
||||
|
||||
// QMF analysis polyphase accumulate, two bands per iteration.
|
||||
void qmf_analysis_taps_neon(double* out, const double* source, const double* coefficients,
|
||||
std::size_t rows) noexcept {
|
||||
for (std::size_t row = 0u; row < rows; ++row) {
|
||||
double* destination = out + row * kQmfAnalysisBands;
|
||||
const double* values = source + row * kQmfAnalysisBands;
|
||||
for (std::size_t band = 0u; band < kQmfAnalysisBands; band += 2u) {
|
||||
vst1q_f64(destination + band,
|
||||
vaddq_f64(vld1q_f64(destination + band),
|
||||
vmulq_f64(vld1q_f64(values + band),
|
||||
vld1q_f64(coefficients + band))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Element-wise complex product of two runs, two complexes per iteration.
|
||||
void complex_multiply_neon(double* out, const double* left, const double* right,
|
||||
std::size_t count) noexcept {
|
||||
std::size_t index = 0u;
|
||||
for (; index + 2u <= count; index += 2u) {
|
||||
const float64x2x2_t first = vld2q_f64(left + index * 2u);
|
||||
const float64x2x2_t second = vld2q_f64(right + index * 2u);
|
||||
float64x2x2_t result;
|
||||
result.val[0] = vsubq_f64(vmulq_f64(first.val[0], second.val[0]),
|
||||
vmulq_f64(first.val[1], second.val[1]));
|
||||
result.val[1] = vaddq_f64(vmulq_f64(first.val[0], second.val[1]),
|
||||
vmulq_f64(first.val[1], second.val[0]));
|
||||
vst2q_f64(out + index * 2u, result);
|
||||
}
|
||||
for (; index < count; ++index) {
|
||||
const std::size_t offset = index * 2u;
|
||||
const double left_real = left[offset];
|
||||
const double left_imag = left[offset + 1u];
|
||||
const double right_real = right[offset];
|
||||
const double right_imag = right[offset + 1u];
|
||||
out[offset] = left_real * right_real - left_imag * right_imag;
|
||||
out[offset + 1u] = left_real * right_imag + left_imag * right_real;
|
||||
}
|
||||
}
|
||||
|
||||
const Kernels kNeon{
|
||||
&fft_butterflies_neon,
|
||||
&qmf_synthesis_basis_neon,
|
||||
&hybrid_low_join_neon,
|
||||
&render_hybrid_path_neon,
|
||||
&complex_axpy_neon,
|
||||
&qmf_analysis_taps_neon,
|
||||
&complex_multiply_neon,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
const Kernels& kernels_intrin_neon() noexcept { return kNeon; }
|
||||
|
||||
} // namespace joc::simd
|
||||
@@ -0,0 +1,156 @@
|
||||
// Scalar reference implementations of the dispatched DSP kernels.
|
||||
//
|
||||
// This unit is compiled with the baseline ISA of the target (no /arch flag on
|
||||
// MSVC, no -m flag elsewhere) and is the ground truth every vector path is
|
||||
// compared against: JOC_SIMD=scalar selects exactly these functions, and their
|
||||
// output must be bit-identical to any ISA path's.
|
||||
//
|
||||
// The arithmetic below is deliberately written with std::complex<double>, which
|
||||
// on MSVC expands to the plain `re * rr - im * ri` / `re * ri + im * rr` pair
|
||||
// (no helper call, no scaling trick), so the vector paths have an unambiguous
|
||||
// two-roundings-per-component target to reproduce.
|
||||
|
||||
#include "simd/simd.h"
|
||||
|
||||
#include <complex>
|
||||
|
||||
#include "foundation/fft.h"
|
||||
|
||||
namespace joc::simd {
|
||||
namespace {
|
||||
|
||||
using Complex = dsp::Complex;
|
||||
|
||||
// Exactly the stage loop of FftPlan::apply, over a table this kernel does not own,
|
||||
// for any power-of-two size. The bit-reversal permutation stays with the caller:
|
||||
// both call sites already write their input in permuted order.
|
||||
void fft_butterflies_scalar(double* raw_data, std::size_t size, const double* raw_twiddle,
|
||||
const std::size_t* stage_begin) noexcept {
|
||||
auto* data = reinterpret_cast<Complex*>(raw_data);
|
||||
const auto* twiddle = reinterpret_cast<const Complex*>(raw_twiddle);
|
||||
std::size_t stage = 0u;
|
||||
for (std::size_t length = 2u; length <= size; length <<= 1u, ++stage) {
|
||||
const Complex* table = twiddle + stage_begin[stage];
|
||||
for (std::size_t start = 0u; start < size; start += length) {
|
||||
for (std::size_t offset = 0u; offset < length / 2u; ++offset) {
|
||||
const Complex even = data[start + offset];
|
||||
const Complex odd = data[start + offset + length / 2u] * table[offset];
|
||||
data[start + offset] = even + odd;
|
||||
data[start + offset + length / 2u] = even - odd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The reference for the synthesis basis: one accumulator per output, taps in
|
||||
// increasing order, one rounding per multiply and per add. This is the shape
|
||||
// both callers write, with the basis rows reordered rank-minor -- the products
|
||||
// and their order are the caller's.
|
||||
void qmf_synthesis_basis_scalar(const double* values, const double* basis, double* out,
|
||||
std::size_t rows) noexcept {
|
||||
for (std::size_t row = 0u; row < rows; ++row) {
|
||||
const double* source = values + row * kSynthesisTaps;
|
||||
double* destination = out + row * kSynthesisBands * kSynthesisRanks;
|
||||
for (std::size_t band = 0u; band < kSynthesisBands; ++band) {
|
||||
const double* table = basis + band * kSynthesisTaps * kSynthesisRanks;
|
||||
for (std::size_t rank = 0u; rank < kSynthesisRanks; ++rank) {
|
||||
double sum = 0.0;
|
||||
for (std::size_t tap = 0u; tap < kSynthesisTaps; ++tap) {
|
||||
sum += source[tap] * table[tap * kSynthesisRanks + rank];
|
||||
}
|
||||
destination[band * kSynthesisRanks + rank] = sum;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The reference for the hybrid low join: one accumulator per output, terms in the
|
||||
// caller's order, one rounding per multiply and per add, zero terms skipped.
|
||||
void hybrid_low_join_scalar(const double* values, const double* kernel, double* out,
|
||||
std::size_t rows) noexcept {
|
||||
for (std::size_t row = 0u; row < rows; ++row) {
|
||||
const double* source = values + row * kHybridTerms;
|
||||
double* destination = out + row * kHybridOutputs;
|
||||
for (std::size_t output = 0u; output < kHybridOutputs; ++output) {
|
||||
destination[output] = 0.0;
|
||||
}
|
||||
for (std::size_t term = 0u; term < kHybridTerms; ++term) {
|
||||
const double value = source[term];
|
||||
if (value == 0.0) {
|
||||
continue;
|
||||
}
|
||||
const double* weights = kernel + term * kHybridOutputs;
|
||||
for (std::size_t output = 0u; output < kHybridOutputs; ++output) {
|
||||
destination[output] += value * weights[output];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The reference for one rendered path: each band of each ear accumulates its own
|
||||
// value, in the caller's order, with the scale applied to each product.
|
||||
void render_hybrid_path_scalar(double* out, const double* transfer, const double* history0,
|
||||
const double* history1, double scale) noexcept {
|
||||
const double* histories[2] = {history0, history1};
|
||||
for (std::size_t ear = 0u; ear < 2u; ++ear) {
|
||||
const double* source = histories[ear];
|
||||
const double* taps = transfer + ear * kPathBands * 2u;
|
||||
double* destination = out + ear * kPathBands * 2u;
|
||||
for (std::size_t band = 0u; band < kPathBands; ++band) {
|
||||
const double source_real = source[band * 2u];
|
||||
const double source_imag = source[band * 2u + 1u];
|
||||
const double tap_real = taps[band * 2u];
|
||||
const double tap_imag = taps[band * 2u + 1u];
|
||||
destination[band * 2u] +=
|
||||
source_real * tap_real * scale - source_imag * tap_imag * scale;
|
||||
destination[band * 2u + 1u] +=
|
||||
source_real * tap_imag * scale + source_imag * tap_real * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Neighbouring complexes and neighbouring bands are independent outputs, so the
|
||||
// references for the two accumulates are plain element-wise loops.
|
||||
void complex_axpy_scalar(double* out, const double* field, double scale,
|
||||
std::size_t count) noexcept {
|
||||
for (std::size_t index = 0u; index < count * 2u; ++index) {
|
||||
out[index] += field[index] * scale;
|
||||
}
|
||||
}
|
||||
|
||||
void qmf_analysis_taps_scalar(double* out, const double* source, const double* coefficients,
|
||||
std::size_t rows) noexcept {
|
||||
for (std::size_t row = 0u; row < rows; ++row) {
|
||||
double* destination = out + row * kQmfAnalysisBands;
|
||||
const double* values = source + row * kQmfAnalysisBands;
|
||||
for (std::size_t band = 0u; band < kQmfAnalysisBands; ++band) {
|
||||
destination[band] += values[band] * coefficients[band];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void complex_multiply_scalar(double* out, const double* left, const double* right,
|
||||
std::size_t count) noexcept {
|
||||
auto* destination = reinterpret_cast<Complex*>(out);
|
||||
const auto* first = reinterpret_cast<const Complex*>(left);
|
||||
const auto* second = reinterpret_cast<const Complex*>(right);
|
||||
for (std::size_t index = 0u; index < count; ++index) {
|
||||
destination[index] = first[index] * second[index];
|
||||
}
|
||||
}
|
||||
|
||||
const Kernels kScalar{
|
||||
&fft_butterflies_scalar,
|
||||
&qmf_synthesis_basis_scalar,
|
||||
&hybrid_low_join_scalar,
|
||||
&render_hybrid_path_scalar,
|
||||
&complex_axpy_scalar,
|
||||
&qmf_analysis_taps_scalar,
|
||||
&complex_multiply_scalar,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
const Kernels& kernels_scalar() noexcept { return kScalar; }
|
||||
|
||||
} // namespace joc::simd
|
||||
@@ -0,0 +1,209 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
// Runtime-dispatched SIMD kernels for the DSP core.
|
||||
//
|
||||
// Module layout (the ISA lives in the file name, never in a subdirectory):
|
||||
//
|
||||
// simd.h this contract -- the only header a caller includes
|
||||
// cpu_probe.{h,cpp} "can this CPU and OS run ISA X", no policy
|
||||
// dispatch.cpp JOC_SIMD + the ladder + the kernel table
|
||||
// kernels_scalar.cpp the reference implementation, Isa::scalar
|
||||
// kernels_intrin_avx2.cpp /arch:AVX2 -mavx2 -- exactly the
|
||||
// kernels_intrin_avx512.cpp /arch:AVX512 -mavx512f -- `kernels_intrin_*`
|
||||
// kernels_intrin_neon.cpp AArch64 default -- units get a wider flag
|
||||
//
|
||||
// The scalar reference in kernels_scalar.cpp fixes both the data layout and the
|
||||
// arithmetic: a vector path may only compute *independent* outputs in parallel
|
||||
// lanes. It may never reassociate an accumulation, contract a multiply into an
|
||||
// FMA, or change the order in which one output's terms are summed, so every ISA
|
||||
// path reproduces the scalar bits exactly and JOC_SIMD=scalar is a valid
|
||||
// reference for any of them. Kernel-level layout changes (AoS -> SoA inside a
|
||||
// kernel, for instance) are fine as long as each individual output value is
|
||||
// still produced by the same sequence of roundings.
|
||||
//
|
||||
// MSVC has no function-level ISA attribute, so each ISA lives in its own
|
||||
// translation unit compiled with its own /arch (or -m) flag and the dispatcher
|
||||
// picks one at run time: CPUID + XGETBV on x86-64, the architectural AArch64
|
||||
// baseline (NEON/ASIMD needs no probing) elsewhere.
|
||||
//
|
||||
// The ISA units are linked into the same binaries as the baseline code, so they
|
||||
// must stay free of objects with dynamic initialisation: a global constructor
|
||||
// would execute ISA-specific instructions before the dispatcher has had a chance
|
||||
// to look at the CPU. Constant tables are fine -- they live in .rdata.
|
||||
namespace joc::simd {
|
||||
|
||||
enum class Isa {
|
||||
scalar = 0,
|
||||
sse2,
|
||||
avx2,
|
||||
avx512,
|
||||
neon,
|
||||
};
|
||||
|
||||
// One entry per dispatched kernel.
|
||||
enum class Kernel {
|
||||
// Power-of-two radix-2 DIT butterfly cascade, in place, over `size` interleaved
|
||||
// (re, im) pairs already in bit-reversed order. `twiddle` is the caller's own
|
||||
// factor table, read as interleaved pairs: stage s (length 2^(s+1)) takes its
|
||||
// 2^s factors starting at stage_begin[s], a *complex* index into `twiddle`.
|
||||
fft_butterflies = 0,
|
||||
// QMF synthesis basis application, see kSynthesis* below.
|
||||
qmf_synthesis_basis,
|
||||
// Hybrid analysis low join, see kHybrid* below.
|
||||
hybrid_low_join,
|
||||
// Hybrid-domain path rendering, see kPathBands below.
|
||||
render_hybrid_path,
|
||||
// Scalar-times-vector accumulate over interleaved complexes.
|
||||
complex_axpy,
|
||||
// QMF analysis polyphase accumulate, see kQmfAnalysisBands below.
|
||||
qmf_analysis_taps,
|
||||
// Element-wise complex product of two whole runs.
|
||||
complex_multiply,
|
||||
count,
|
||||
};
|
||||
|
||||
// Both filterbanks apply the same map: for every output row and every one of the
|
||||
// 64 bands, four ranks each accumulate 128 taps.
|
||||
inline constexpr std::size_t kSynthesisBands = 64;
|
||||
inline constexpr std::size_t kSynthesisRanks = 4;
|
||||
inline constexpr std::size_t kSynthesisTaps = 128;
|
||||
|
||||
// ... and both derive the same 16 low hybrid bands from 78 terms: 3 parents x 2
|
||||
// components x 13 lags.
|
||||
inline constexpr std::size_t kHybridTerms = 78;
|
||||
inline constexpr std::size_t kHybridOutputs = 32;
|
||||
// Rows the callers stage at a time: enough to keep the kernel busy, small enough
|
||||
// that the staged values stay in the first-level cache.
|
||||
inline constexpr std::size_t kHybridJoinBlock = 32;
|
||||
|
||||
// Bands in one ear of a rendered path (the SOFA renderer's 77-band hybrid domain).
|
||||
inline constexpr std::size_t kPathBands = 77;
|
||||
|
||||
// The cascade packs whole groups into a vector, so it needs at least one full
|
||||
// packing: a size of 8 covers a 512-bit stage. Smaller transforms are rare enough
|
||||
// (and short enough) that the caller's portable loop is the right answer there.
|
||||
inline constexpr std::size_t kMinVectorFftSize = 8;
|
||||
|
||||
// Bands in one QMF analysis row (the polyphase accumulator of both filterbanks).
|
||||
inline constexpr std::size_t kQmfAnalysisBands = 64;
|
||||
|
||||
const char* isa_name(Isa isa) noexcept;
|
||||
|
||||
// Whether this CPU *and* the OS state it has to save can execute the ISA. The
|
||||
// answer never changes, so it is computed once.
|
||||
bool isa_supported(Isa isa) noexcept;
|
||||
|
||||
// The ISA the dispatcher settled on, after applying a JOC_SIMD override. This
|
||||
// is the widest ISA considered, not necessarily the one that serves every
|
||||
// kernel: active_isa() answers that per kernel.
|
||||
Isa selected_isa() noexcept;
|
||||
|
||||
// Which ISA actually implements `kernel` after the per-kernel fallback.
|
||||
Isa active_isa(Kernel kernel) noexcept;
|
||||
|
||||
// The dispatched kernels; a slot is never null.
|
||||
struct Kernels {
|
||||
void (*fft_butterflies)(double* data, std::size_t size, const double* twiddle,
|
||||
const std::size_t* stage_begin) noexcept = nullptr;
|
||||
|
||||
// values: [rows][kSynthesisTaps] real/imaginary of the 64 bands of one row.
|
||||
// basis: the same weights the caller holds, reordered rank-minor, so that
|
||||
// (band, tap) addresses its kSynthesisRanks weights contiguously:
|
||||
// basis[(band * kSynthesisTaps + tap) * kSynthesisRanks + rank].
|
||||
// out: [rows][kSynthesisBands][kSynthesisRanks], rank-minor as well.
|
||||
//
|
||||
// Each of the four ranks is an independent dot product over the row, so the
|
||||
// four of them are what shares a vector; every lane keeps the tap order and
|
||||
// the two roundings of the caller's `sum += values[tap] * weight`.
|
||||
void (*qmf_synthesis_basis)(const double* values, const double* basis, double* out,
|
||||
std::size_t rows) noexcept = nullptr;
|
||||
|
||||
// Hybrid analysis low join. Every output row accumulates kHybridTerms values
|
||||
// into kHybridOutputs outputs (16 bands x a real/imaginary pair, component
|
||||
// minor):
|
||||
// out[row][output] = sum over term of values[row][term] * kernel[term][output]
|
||||
// values is [rows][kHybridTerms] in the caller's own term order and kernel is
|
||||
// the caller's taps regrouped to that same order, so that one term's 32
|
||||
// weights are contiguous. Terms whose value is exactly zero are skipped, as
|
||||
// both callers do: adding a zero product to a lane can only leave it alone
|
||||
// (no lane's running sum can be a negative zero, since it starts at +0 and
|
||||
// sums without ever producing one).
|
||||
void (*hybrid_low_join)(const double* values, const double* kernel, double* out,
|
||||
std::size_t rows) noexcept = nullptr;
|
||||
|
||||
// One rendered path. For each ear e and band b, with the ear's history sample
|
||||
// h and the path's transfer t:
|
||||
// out[e][b].re += h.re * t[e][b].re * scale - h.im * t[e][b].im * scale
|
||||
// out[e][b].im += h.re * t[e][b].im * scale + h.im * t[e][b].re * scale
|
||||
// `history0` and `history1` are the two ears' [kPathBands] interleaved complex
|
||||
// rows (they come from different places in the history ring); `transfer` and
|
||||
// `out` are [2][kPathBands] interleaved complexes. The scale multiplies each
|
||||
// product separately, exactly as the caller writes it, so the bands -- which
|
||||
// are independent accumulations into independent outputs -- are what the lanes
|
||||
// carry.
|
||||
void (*render_hybrid_path)(double* out, const double* transfer, const double* history0,
|
||||
const double* history1, double scale) noexcept = nullptr;
|
||||
|
||||
// `count` interleaved complexes, accumulated in place:
|
||||
// out[i] += field[i] * scale
|
||||
// Every element is an independent accumulation of one product, so the lanes
|
||||
// carry neighbouring elements and each one keeps the caller's multiply-then-add.
|
||||
void (*complex_axpy)(double* out, const double* field, double scale,
|
||||
std::size_t count) noexcept = nullptr;
|
||||
|
||||
// QMF analysis polyphase accumulate: `rows` rows of kQmfAnalysisBands bands,
|
||||
// out[row][band] += source[row][band] * coefficients[band]
|
||||
// Neighbouring bands are neighbouring outputs, so they are what fills a vector;
|
||||
// each band accumulates its own product once, in the caller's order.
|
||||
void (*qmf_analysis_taps)(double* out, const double* source, const double* coefficients,
|
||||
std::size_t rows) noexcept = nullptr;
|
||||
|
||||
// `count` interleaved complexes, multiplied element by element:
|
||||
// out[i] = left[i] * right[i]
|
||||
// with the caller's `re * re - im * im` and `re * im + im * re`, two roundings
|
||||
// per component. Neighbouring complexes are independent products.
|
||||
void (*complex_multiply)(double* out, const double* left, const double* right,
|
||||
std::size_t count) noexcept = nullptr;
|
||||
};
|
||||
|
||||
const Kernels& kernels() noexcept;
|
||||
|
||||
// Convenience wrappers.
|
||||
inline void fft_butterflies(double* data, std::size_t size, const double* twiddle,
|
||||
const std::size_t* stage_begin) noexcept {
|
||||
kernels().fft_butterflies(data, size, twiddle, stage_begin);
|
||||
}
|
||||
|
||||
inline void qmf_synthesis_basis(const double* values, const double* basis, double* out,
|
||||
std::size_t rows) noexcept {
|
||||
kernels().qmf_synthesis_basis(values, basis, out, rows);
|
||||
}
|
||||
|
||||
inline void hybrid_low_join(const double* values, const double* kernel, double* out,
|
||||
std::size_t rows) noexcept {
|
||||
kernels().hybrid_low_join(values, kernel, out, rows);
|
||||
}
|
||||
|
||||
inline void render_hybrid_path(double* out, const double* transfer, const double* history0,
|
||||
const double* history1, double scale) noexcept {
|
||||
kernels().render_hybrid_path(out, transfer, history0, history1, scale);
|
||||
}
|
||||
|
||||
inline void complex_axpy(double* out, const double* field, double scale,
|
||||
std::size_t count) noexcept {
|
||||
kernels().complex_axpy(out, field, scale, count);
|
||||
}
|
||||
|
||||
inline void qmf_analysis_taps(double* out, const double* source, const double* coefficients,
|
||||
std::size_t rows) noexcept {
|
||||
kernels().qmf_analysis_taps(out, source, coefficients, rows);
|
||||
}
|
||||
|
||||
inline void complex_multiply(double* out, const double* left, const double* right,
|
||||
std::size_t count) noexcept {
|
||||
kernels().complex_multiply(out, left, right, count);
|
||||
}
|
||||
|
||||
} // namespace joc::simd
|
||||
@@ -0,0 +1,71 @@
|
||||
#include "speaker/speaker_layout_lookup.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace joc::speaker {
|
||||
|
||||
namespace {
|
||||
|
||||
struct FrozenLayout {
|
||||
const char* name;
|
||||
std::uint32_t out_ch_config;
|
||||
std::uint32_t speaker_bitfield;
|
||||
};
|
||||
|
||||
constexpr FrozenLayout kLayouts[kLayoutCount] = {
|
||||
{"2.0", 0, 1},
|
||||
{"3.1", 3, 7},
|
||||
{"5.1", 7, 15},
|
||||
{"7.1", 11, 31},
|
||||
{"5.1.2", 13, 1039},
|
||||
{"5.1.4", 14, 2575},
|
||||
{"7.1.2", 15, 1055},
|
||||
{"7.1.4", 16, 2591},
|
||||
{"9.1.4", 19, 2719},
|
||||
{"9.1.6", 20, 3743},
|
||||
};
|
||||
|
||||
void fill(const FrozenLayout& source, LayoutInfo* out) {
|
||||
out->name = source.name;
|
||||
out->out_ch_config = source.out_ch_config;
|
||||
out->speaker_bitfield = source.speaker_bitfield;
|
||||
out->channel_count = ejoc_speaker_layout_channel_count(source.speaker_bitfield);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool layout_at(int index, LayoutInfo* out) {
|
||||
if (out == nullptr || index < 0 || index >= kLayoutCount) {
|
||||
return false;
|
||||
}
|
||||
fill(kLayouts[index], out);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool layout_by_name(const char* name, LayoutInfo* out) {
|
||||
if (name == nullptr || out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < kLayoutCount; ++index) {
|
||||
if (std::strcmp(name, kLayouts[index].name) == 0) {
|
||||
fill(kLayouts[index], out);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool layout_by_bitfield(std::uint32_t bitfield, LayoutInfo* out) {
|
||||
if (out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < kLayoutCount; ++index) {
|
||||
if (kLayouts[index].speaker_bitfield == bitfield) {
|
||||
fill(kLayouts[index], out);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace joc::speaker
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "eac3joc_core.h"
|
||||
#include "joc_core.h"
|
||||
|
||||
namespace joc::speaker {
|
||||
|
||||
inline constexpr int kLayoutCount = 10;
|
||||
|
||||
struct LayoutInfo {
|
||||
const char* name = "";
|
||||
std::uint32_t out_ch_config = 0;
|
||||
std::uint32_t speaker_bitfield = 0;
|
||||
std::uint32_t channel_count = 0;
|
||||
};
|
||||
|
||||
// All ten layouts in the reference's order (2.0, 3.1, 5.1, 7.1, 5.1.2, 5.1.4,
|
||||
bool layout_at(int index, LayoutInfo* out);
|
||||
|
||||
// False when the name is unknown (case-sensitive, exactly as the reference CLI).
|
||||
bool layout_by_name(const char* name, LayoutInfo* out);
|
||||
|
||||
bool layout_by_bitfield(std::uint32_t bitfield, LayoutInfo* out);
|
||||
|
||||
} // namespace joc::speaker
|
||||
@@ -0,0 +1,534 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace ejoc::speaker_tables {
|
||||
|
||||
inline constexpr std::size_t kMaxPoints = 15;
|
||||
inline constexpr std::size_t kMaxGroups = 4;
|
||||
inline constexpr std::size_t kMaxGroupSize = 3;
|
||||
inline constexpr std::size_t kMaxChannels = 16;
|
||||
|
||||
struct SpeakerPoint {
|
||||
std::array<std::uint16_t, 3> coordinate_q15{};
|
||||
std::uint8_t speaker_id{};
|
||||
};
|
||||
|
||||
struct AxisGroup {
|
||||
std::uint8_t size{};
|
||||
std::array<std::uint8_t, kMaxGroupSize> indices{};
|
||||
};
|
||||
|
||||
struct RegionGeometry {
|
||||
std::uint8_t point_count{};
|
||||
std::uint8_t mode{};
|
||||
std::uint8_t axis0_group_count{};
|
||||
std::uint8_t axis1_group_count{};
|
||||
std::array<SpeakerPoint, kMaxPoints> points{};
|
||||
std::array<AxisGroup, kMaxGroups> axis0_groups{};
|
||||
std::array<AxisGroup, kMaxGroups> axis1_groups{};
|
||||
};
|
||||
|
||||
struct LayoutGeometry {
|
||||
std::uint32_t speaker_bitfield{};
|
||||
std::uint8_t out_ch_config{};
|
||||
std::uint8_t channel_count{};
|
||||
std::array<std::uint8_t, kMaxChannels> standard_from_internal{};
|
||||
std::array<RegionGeometry, 7> regions{};
|
||||
};
|
||||
|
||||
inline constexpr std::array<LayoutGeometry, 10> kLayouts{{
|
||||
LayoutGeometry{
|
||||
0x1u, 0, 2,
|
||||
{{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
|
||||
{{
|
||||
RegionGeometry{
|
||||
2, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
2, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
2, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
2, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
2, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
2, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
2, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
}
|
||||
}}
|
||||
},
|
||||
LayoutGeometry{
|
||||
0x7u, 3, 4,
|
||||
{{0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
|
||||
{{
|
||||
RegionGeometry{
|
||||
3, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
3, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
3, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
1, 1, 1, 0,
|
||||
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
3, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
3, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
3, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
}
|
||||
}}
|
||||
},
|
||||
LayoutGeometry{
|
||||
0xFu, 7, 6,
|
||||
{{0, 1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
|
||||
{{
|
||||
RegionGeometry{
|
||||
5, 2, 2, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
5, 2, 2, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
5, 2, 2, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
3, 2, 2, 0,
|
||||
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
3, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
2, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
3, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
}
|
||||
}}
|
||||
},
|
||||
LayoutGeometry{
|
||||
0x1Fu, 11, 8,
|
||||
{{0, 1, 2, 3, 6, 7, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0}},
|
||||
{{
|
||||
RegionGeometry{
|
||||
7, 2, 3, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
5, 2, 2, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
5, 2, 2, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
3, 2, 2, 0,
|
||||
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
3, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
4, 2, 2, 0,
|
||||
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
3, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
}
|
||||
}}
|
||||
},
|
||||
LayoutGeometry{
|
||||
0x40Fu, 13, 8,
|
||||
{{0, 1, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0}},
|
||||
{{
|
||||
RegionGeometry{
|
||||
7, 3, 2, 1,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
7, 3, 2, 1,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
7, 3, 2, 1,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
5, 3, 2, 1,
|
||||
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
5, 3, 1, 1,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
4, 3, 1, 1,
|
||||
{{SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
3, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
}
|
||||
}}
|
||||
},
|
||||
LayoutGeometry{
|
||||
0xA0Fu, 14, 10,
|
||||
{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0}},
|
||||
{{
|
||||
RegionGeometry{
|
||||
9, 3, 2, 2,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
9, 3, 2, 2,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
9, 3, 2, 2,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
7, 3, 2, 2,
|
||||
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
7, 3, 1, 2,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
6, 3, 1, 2,
|
||||
{{SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {2, 3, 0}}, AxisGroup{2, {4, 5, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
5, 3, 1, 1,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
}
|
||||
}}
|
||||
},
|
||||
LayoutGeometry{
|
||||
0x41Fu, 15, 10,
|
||||
{{0, 1, 2, 3, 6, 7, 4, 5, 8, 9, 0, 0, 0, 0, 0, 0}},
|
||||
{{
|
||||
RegionGeometry{
|
||||
9, 3, 3, 1,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
7, 3, 2, 1,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
7, 3, 2, 1,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
5, 3, 2, 1,
|
||||
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
5, 3, 1, 1,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
6, 3, 2, 1,
|
||||
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {4, 5, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
3, 1, 1, 0,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
}
|
||||
}}
|
||||
},
|
||||
LayoutGeometry{
|
||||
0xA1Fu, 16, 12,
|
||||
{{0, 1, 2, 3, 6, 7, 4, 5, 8, 9, 10, 11, 0, 0, 0, 0}},
|
||||
{{
|
||||
RegionGeometry{
|
||||
11, 3, 3, 2,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {9, 10, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
9, 3, 2, 2,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
9, 3, 2, 2,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
7, 3, 2, 2,
|
||||
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
7, 3, 1, 2,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
8, 3, 2, 2,
|
||||
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {4, 5, 0}}, AxisGroup{2, {6, 7, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
5, 3, 1, 1,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
}
|
||||
}}
|
||||
},
|
||||
LayoutGeometry{
|
||||
0xA9Fu, 19, 14,
|
||||
{{0, 1, 2, 3, 6, 7, 4, 5, 10, 11, 12, 13, 8, 9, 0, 0}},
|
||||
{{
|
||||
RegionGeometry{
|
||||
13, 3, 4, 2,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}}},
|
||||
{{AxisGroup{2, {9, 10, 0}}, AxisGroup{2, {11, 12, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
11, 3, 3, 2,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {9, 10, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
9, 3, 2, 2,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
7, 3, 2, 2,
|
||||
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
7, 3, 1, 2,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
8, 3, 2, 2,
|
||||
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {4, 5, 0}}, AxisGroup{2, {6, 7, 0}}, AxisGroup{}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
7, 3, 2, 1,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
}
|
||||
}}
|
||||
},
|
||||
LayoutGeometry{
|
||||
0xE9Fu, 20, 16,
|
||||
{{0, 1, 2, 3, 6, 7, 4, 5, 10, 11, 14, 15, 12, 13, 8, 9}},
|
||||
{{
|
||||
RegionGeometry{
|
||||
15, 3, 4, 3,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}}},
|
||||
{{AxisGroup{2, {9, 10, 0}}, AxisGroup{2, {11, 12, 0}}, AxisGroup{2, {13, 14, 0}}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
13, 3, 3, 3,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {9, 10, 0}}, AxisGroup{2, {11, 12, 0}}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
11, 3, 2, 3,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {9, 10, 0}}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
9, 3, 2, 3,
|
||||
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
9, 3, 1, 3,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
10, 3, 2, 3,
|
||||
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {4, 5, 0}}, AxisGroup{2, {6, 7, 0}}, AxisGroup{2, {8, 9, 0}}, AxisGroup{}}}
|
||||
},
|
||||
RegionGeometry{
|
||||
7, 3, 2, 1,
|
||||
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
|
||||
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
|
||||
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
|
||||
}
|
||||
}}
|
||||
}
|
||||
}};
|
||||
|
||||
inline constexpr const LayoutGeometry* find_layout(const std::uint32_t speaker_bitfield) noexcept {
|
||||
for (const auto& layout : kLayouts) {
|
||||
if (layout.speaker_bitfield == speaker_bitfield) {
|
||||
return &layout;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace ejoc::speaker_tables
|
||||
@@ -0,0 +1,495 @@
|
||||
#define EJOC_BUILD_DLL
|
||||
#include "eac3joc_core.h"
|
||||
#include "speaker_layouts.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <new>
|
||||
|
||||
namespace ejoc::speaker {
|
||||
|
||||
using speaker_tables::AxisGroup;
|
||||
using speaker_tables::LayoutGeometry;
|
||||
using speaker_tables::RegionGeometry;
|
||||
|
||||
constexpr double kPi = 3.141592653589793238462643383279502884;
|
||||
constexpr double kQ15Scale = 32768.0;
|
||||
constexpr double kQ15Max = 32767.0 / kQ15Scale;
|
||||
constexpr double kGainSnapThreshold = 1.0e-4;
|
||||
constexpr std::size_t kObjects = EJOC_MAX_OBJECTS;
|
||||
constexpr std::size_t kChannels = EJOC_OUTPUT_CHANNELS;
|
||||
constexpr std::size_t kBlock = EJOC_SPEAKER_BLOCK_SAMPLES;
|
||||
|
||||
using PointGains = std::array<double, speaker_tables::kMaxPoints>;
|
||||
using ChannelGains = std::array<double, kChannels>;
|
||||
using ObjectChannelGains = std::array<ChannelGains, kObjects>;
|
||||
using RemainingCounts = std::array<std::array<std::uint32_t, kChannels>, kObjects>;
|
||||
|
||||
inline double clamp(const double value, const double low, const double high) noexcept {
|
||||
return value < low ? low : (value > high ? high : value);
|
||||
}
|
||||
|
||||
inline double coordinate(const RegionGeometry& region, const std::size_t point,
|
||||
const std::size_t component) noexcept {
|
||||
return static_cast<double>(region.points[point].coordinate_q15[component]) / kQ15Scale;
|
||||
}
|
||||
|
||||
std::uint64_t expand_speaker_bitfield(const std::uint32_t compact) noexcept {
|
||||
constexpr std::array<std::uint64_t, 22> expansions{{
|
||||
0x00000003ULL, 0x00000004ULL, 0x00000008ULL, 0x00000030ULL,
|
||||
0x000000C0ULL, 0x00000100ULL, 0x00000600ULL, 0x00001800ULL,
|
||||
0x00006000ULL, 0x00018000ULL, 0x00060000ULL, 0x00180000ULL,
|
||||
0x00600000ULL, 0x01800000ULL, 0x06000000ULL, 0x18000000ULL,
|
||||
0x60000000ULL, 0x080000000ULL, 0x600000000ULL, 0x800000000ULL,
|
||||
0x1000000000ULL, 0x2000000000ULL,
|
||||
}};
|
||||
std::uint64_t expanded = 0;
|
||||
for (std::size_t bit_index = 0; bit_index < expansions.size(); ++bit_index) {
|
||||
if ((compact & (1u << bit_index)) != 0) {
|
||||
expanded |= expansions[bit_index];
|
||||
}
|
||||
}
|
||||
return expanded;
|
||||
}
|
||||
|
||||
inline int bit(const std::uint64_t value, const unsigned index) noexcept {
|
||||
return static_cast<int>((value >> index) & 1ULL);
|
||||
}
|
||||
|
||||
double layout_attenuation_db(const std::uint32_t compact) noexcept {
|
||||
const std::uint64_t expanded = expand_speaker_bitfield(compact);
|
||||
const int height_channels = 2 * (
|
||||
bit(expanded, 13) + bit(expanded, 15) + bit(expanded, 17) +
|
||||
bit(expanded, 19) + bit(expanded, 21));
|
||||
const int floor_channels = bit(expanded, 8) + 2 * (
|
||||
bit(expanded, 31) + bit(expanded, 4) + bit(expanded, 6) +
|
||||
bit(expanded, 11) + bit(expanded, 25) + bit(expanded, 27) +
|
||||
bit(expanded, 29) + bit(expanded, 33));
|
||||
const double height_factor = std::min(static_cast<double>(height_channels) / 4.0, 1.0);
|
||||
const double floor_factor = std::min(static_cast<double>(floor_channels) / 4.0, 1.0);
|
||||
return -std::max(4.5 - 1.5 * height_factor - 3.0 * floor_factor, 0.0);
|
||||
}
|
||||
|
||||
int floor_y_exponent(const std::uint32_t compact) noexcept {
|
||||
const std::uint32_t low = static_cast<std::uint32_t>(expand_speaker_bitfield(compact));
|
||||
return ((low & 0x130u) != 0 && (low & 0x18C0u) == 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
inline void equal_power_pair(const double position, double& lower, double& upper) noexcept {
|
||||
const double angle = (kPi * 0.5) * position;
|
||||
lower = std::cos(angle);
|
||||
upper = std::sin(angle);
|
||||
}
|
||||
|
||||
void axis0_gains(const RegionGeometry& region,
|
||||
const std::array<AxisGroup, speaker_tables::kMaxGroups>& groups,
|
||||
const std::uint8_t group_count,
|
||||
const double value,
|
||||
PointGains& output) noexcept {
|
||||
output.fill(0.0);
|
||||
for (std::size_t row = 0; row < group_count; ++row) {
|
||||
const auto& group = groups[row];
|
||||
if (group.size == 0) {
|
||||
continue;
|
||||
}
|
||||
const std::size_t first = group.indices[0];
|
||||
const std::size_t last = group.indices[group.size - 1];
|
||||
const double first_value = coordinate(region, first, 0);
|
||||
const double last_value = coordinate(region, last, 0);
|
||||
if (value <= first_value) {
|
||||
output[first] = 1.0;
|
||||
continue;
|
||||
}
|
||||
if (value >= last_value) {
|
||||
output[last] = 1.0;
|
||||
continue;
|
||||
}
|
||||
for (std::size_t index = 0; index + 1 < group.size; ++index) {
|
||||
const std::size_t lower_index = group.indices[index];
|
||||
const std::size_t upper_index = group.indices[index + 1];
|
||||
const double lower_value = coordinate(region, lower_index, 0);
|
||||
const double upper_value = coordinate(region, upper_index, 0);
|
||||
if (value > lower_value && value <= upper_value) {
|
||||
const double position = (value - lower_value) / (upper_value - lower_value);
|
||||
equal_power_pair(position, output[lower_index], output[upper_index]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void axis1_gains(const RegionGeometry& region,
|
||||
const std::array<AxisGroup, speaker_tables::kMaxGroups>& groups,
|
||||
const std::uint8_t group_count,
|
||||
const double value,
|
||||
PointGains& output) noexcept {
|
||||
output.fill(0.0);
|
||||
if (group_count == 0) {
|
||||
return;
|
||||
}
|
||||
const auto& first_group = groups[0];
|
||||
const auto& last_group = groups[group_count - 1];
|
||||
const double first_value = coordinate(region, first_group.indices[0], 1);
|
||||
const double last_value = coordinate(region, last_group.indices[0], 1);
|
||||
if (value <= first_value) {
|
||||
for (std::size_t index = 0; index < first_group.size; ++index) {
|
||||
output[first_group.indices[index]] = 1.0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (value > last_value) {
|
||||
for (std::size_t index = 0; index < last_group.size; ++index) {
|
||||
output[last_group.indices[index]] = 1.0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (std::size_t row = 0; row + 1 < group_count; ++row) {
|
||||
const auto& lower_group = groups[row];
|
||||
const auto& upper_group = groups[row + 1];
|
||||
const double lower_value = coordinate(region, lower_group.indices[0], 1);
|
||||
const double upper_value = coordinate(region, upper_group.indices[0], 1);
|
||||
if (value >= lower_value && value <= upper_value) {
|
||||
const double position = (value - lower_value) / (upper_value - lower_value);
|
||||
double lower_gain = 0.0;
|
||||
double upper_gain = 0.0;
|
||||
equal_power_pair(position, lower_gain, upper_gain);
|
||||
for (std::size_t index = 0; index < lower_group.size; ++index) {
|
||||
output[lower_group.indices[index]] = lower_gain;
|
||||
}
|
||||
for (std::size_t index = 0; index < upper_group.size; ++index) {
|
||||
output[upper_group.indices[index]] = upper_gain;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void plane_gains(const RegionGeometry& region,
|
||||
const std::array<AxisGroup, speaker_tables::kMaxGroups>& groups,
|
||||
const std::uint8_t group_count,
|
||||
const double u,
|
||||
const double v,
|
||||
const std::uint8_t mode,
|
||||
PointGains& output) noexcept {
|
||||
axis0_gains(region, groups, group_count, u, output);
|
||||
if (mode >= 2) {
|
||||
PointGains vertical{};
|
||||
axis1_gains(region, groups, group_count, v, vertical);
|
||||
for (std::size_t point = 0; point < region.point_count; ++point) {
|
||||
output[point] *= vertical[point];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Renderer {
|
||||
public:
|
||||
explicit Renderer(const LayoutGeometry* layout) noexcept : layout_(layout) {
|
||||
for (std::size_t standard = 0; standard < layout_->channel_count; ++standard) {
|
||||
standard_index_for_internal_[layout_->standard_from_internal[standard]] =
|
||||
static_cast<std::uint8_t>(standard);
|
||||
}
|
||||
attenuation_db_ = layout_attenuation_db(layout_->speaker_bitfield);
|
||||
floor_y_exponent_ = floor_y_exponent(layout_->speaker_bitfield);
|
||||
reset_state();
|
||||
}
|
||||
|
||||
int reset() noexcept {
|
||||
reset_state();
|
||||
error_[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* last_error() const noexcept {
|
||||
return error_[0] ? error_.data() : "";
|
||||
}
|
||||
|
||||
int process(const float* input,
|
||||
const std::uint32_t sample_count,
|
||||
const std::uint32_t metadata_count,
|
||||
const std::uint32_t* metadata_offsets,
|
||||
const std::uint32_t* ramp_durations,
|
||||
const std::uint16_t* positions_q15,
|
||||
const std::uint8_t* region_indices,
|
||||
const std::uint8_t* height_enabled,
|
||||
const double* object_gains,
|
||||
double* output) noexcept {
|
||||
error_[0] = '\0';
|
||||
if (!input || !output) {
|
||||
return fail("null PCM pointer passed to ejoc_speaker_renderer_process");
|
||||
}
|
||||
if ((sample_count % kBlock) != 0) {
|
||||
return fail("sample_count must be a multiple of 32");
|
||||
}
|
||||
if (metadata_count && (!metadata_offsets || !ramp_durations || !positions_q15)) {
|
||||
return fail("metadata arrays are null while metadata_count is nonzero");
|
||||
}
|
||||
if (!validate_metadata(sample_count, metadata_count, metadata_offsets,
|
||||
positions_q15, region_indices, object_gains)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::fill(output, output + static_cast<std::size_t>(sample_count) * layout_->channel_count, 0.0);
|
||||
const bool has_lfe = (layout_->speaker_bitfield & 0x4u) != 0;
|
||||
const std::size_t total_blocks = sample_count / kBlock;
|
||||
std::size_t event = 0;
|
||||
for (std::size_t block = 0; block < total_blocks; ++block) {
|
||||
while (event < metadata_count && aligned_block(metadata_offsets[event]) == block) {
|
||||
apply_event(event, ramp_durations, positions_q15, region_indices,
|
||||
height_enabled, object_gains);
|
||||
++event;
|
||||
}
|
||||
mix_block(input, output, block, has_lfe);
|
||||
}
|
||||
while (event < metadata_count && aligned_block(metadata_offsets[event]) == total_blocks) {
|
||||
apply_event(event, ramp_durations, positions_q15, region_indices,
|
||||
height_enabled, object_gains);
|
||||
++event;
|
||||
}
|
||||
if (event != metadata_count) {
|
||||
return fail("metadata alignment produced an event outside this process call");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
void reset_state() noexcept {
|
||||
for (auto& row : current_) row.fill(0.0);
|
||||
for (auto& row : target_) row.fill(0.0);
|
||||
for (auto& row : step_) row.fill(0.0);
|
||||
for (auto& row : remaining_) row.fill(0);
|
||||
}
|
||||
|
||||
int fail(const char* message) noexcept {
|
||||
std::snprintf(error_.data(), error_.size(), "%s", message);
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool validate_metadata(const std::uint32_t sample_count,
|
||||
const std::uint32_t metadata_count,
|
||||
const std::uint32_t* metadata_offsets,
|
||||
const std::uint16_t* positions_q15,
|
||||
const std::uint8_t* region_indices,
|
||||
const double* object_gains) noexcept {
|
||||
for (std::size_t event = 0; event < metadata_count; ++event) {
|
||||
if (metadata_offsets[event] > sample_count) {
|
||||
fail("metadata offset exceeds sample_count");
|
||||
return false;
|
||||
}
|
||||
if (event && metadata_offsets[event] < metadata_offsets[event - 1]) {
|
||||
fail("metadata offsets must be nondecreasing");
|
||||
return false;
|
||||
}
|
||||
for (std::size_t object = 0; object < kObjects; ++object) {
|
||||
const std::size_t object_event = event * kObjects + object;
|
||||
if (region_indices && region_indices[object_event] >= 7) {
|
||||
fail("region index is above 6");
|
||||
return false;
|
||||
}
|
||||
if (object_gains && !std::isfinite(object_gains[object_event])) {
|
||||
fail("object gain is not finite");
|
||||
return false;
|
||||
}
|
||||
const std::size_t coordinate_base = object_event * EJOC_SPEAKER_COORDINATES;
|
||||
for (std::size_t component = 0; component < EJOC_SPEAKER_COORDINATES; ++component) {
|
||||
if (positions_q15[coordinate_base + component] > 32767u) {
|
||||
fail("Q15 object coordinate is above 32767");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::size_t aligned_block(const std::uint32_t sample) noexcept {
|
||||
return (static_cast<std::size_t>(sample) + kBlock / 2 - 1) / kBlock;
|
||||
}
|
||||
|
||||
static std::uint32_t ramp_blocks(const std::uint32_t duration) noexcept {
|
||||
return static_cast<std::uint32_t>(
|
||||
(static_cast<std::size_t>(duration) + kBlock / 2 - 1) / kBlock);
|
||||
}
|
||||
|
||||
void render_point(const std::uint16_t* position,
|
||||
const std::uint8_t region_index,
|
||||
const bool enable_height,
|
||||
const double object_gain,
|
||||
ChannelGains& output) const noexcept {
|
||||
output.fill(0.0);
|
||||
const auto& region = layout_->regions[region_index];
|
||||
const double u = static_cast<double>(position[0]) / kQ15Scale;
|
||||
const double v = static_cast<double>(position[1]) / kQ15Scale;
|
||||
const double w = static_cast<double>(position[2]) / kQ15Scale;
|
||||
const double floor_v = clamp(std::ldexp(v, floor_y_exponent_), 0.0, 1.0);
|
||||
PointGains floor{};
|
||||
plane_gains(region, region.axis0_groups, region.axis0_group_count,
|
||||
u, floor_v, region.mode, floor);
|
||||
PointGains point = floor;
|
||||
if (region.mode == 3) {
|
||||
PointGains height{};
|
||||
plane_gains(region, region.axis1_groups, region.axis1_group_count,
|
||||
u, v, 3, height);
|
||||
const double z = enable_height ? clamp(w, 0.0, kQ15Max) : 0.0;
|
||||
if (z >= kQ15Max) {
|
||||
point = height;
|
||||
} else if (z > 0.0) {
|
||||
double floor_weight = 0.0;
|
||||
double height_weight = 0.0;
|
||||
equal_power_pair(z, floor_weight, height_weight);
|
||||
for (std::size_t index = 0; index < region.point_count; ++index) {
|
||||
point[index] = floor[index] * floor_weight + height[index] * height_weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
const double y_term = clamp(v / 0.6, 0.0, 1.0);
|
||||
const double z_term = clamp((w - 0.2) / 0.8, 0.0, 1.0);
|
||||
const double amount = clamp(y_term + z_term, 0.0, 1.0);
|
||||
const double gain = std::pow(10.0, attenuation_db_ * amount / 20.0) * object_gain;
|
||||
for (std::size_t index = 0; index < region.point_count; ++index) {
|
||||
output[region.points[index].speaker_id] = point[index] * gain;
|
||||
}
|
||||
}
|
||||
|
||||
void apply_event(const std::size_t event,
|
||||
const std::uint32_t* ramp_durations,
|
||||
const std::uint16_t* positions_q15,
|
||||
const std::uint8_t* region_indices,
|
||||
const std::uint8_t* height_enabled,
|
||||
const double* object_gains) noexcept {
|
||||
const std::uint32_t blocks = ramp_blocks(ramp_durations[event]);
|
||||
for (std::size_t object = 0; object < kObjects; ++object) {
|
||||
const std::size_t object_event = event * kObjects + object;
|
||||
const auto* position = positions_q15 + object_event * EJOC_SPEAKER_COORDINATES;
|
||||
const std::uint8_t region = region_indices ? region_indices[object_event] : 0;
|
||||
const bool height = !height_enabled || height_enabled[object_event] != 0;
|
||||
const double object_gain = object_gains ? object_gains[object_event] : 1.0;
|
||||
ChannelGains next{};
|
||||
render_point(position, region, height, object_gain, next);
|
||||
for (std::size_t channel = 0; channel < layout_->channel_count; ++channel) {
|
||||
const double difference = next[channel] - current_[object][channel];
|
||||
target_[object][channel] = next[channel];
|
||||
if (std::abs(difference) >= kGainSnapThreshold && blocks != 0) {
|
||||
step_[object][channel] = difference / static_cast<double>(blocks);
|
||||
remaining_[object][channel] = blocks;
|
||||
} else {
|
||||
current_[object][channel] = next[channel];
|
||||
step_[object][channel] = 0.0;
|
||||
remaining_[object][channel] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mix_block(const float* input, double* output, const std::size_t block,
|
||||
const bool has_lfe) noexcept {
|
||||
const std::size_t start = block * kBlock;
|
||||
if (has_lfe) {
|
||||
for (std::size_t sample = 0; sample < kBlock; ++sample) {
|
||||
output[(start + sample) * layout_->channel_count + 3] =
|
||||
static_cast<double>(input[(start + sample) * EJOC_OUTPUT_CHANNELS]);
|
||||
}
|
||||
}
|
||||
for (std::size_t object = 0; object < kObjects; ++object) {
|
||||
for (std::size_t internal = 0; internal < layout_->channel_count; ++internal) {
|
||||
const bool active = remaining_[object][internal] != 0;
|
||||
const double fixed_gain = target_[object][internal];
|
||||
if (!active && fixed_gain == 0.0) {
|
||||
continue;
|
||||
}
|
||||
const std::size_t standard = standard_index_for_internal_[internal];
|
||||
for (std::size_t sample = 0; sample < kBlock; ++sample) {
|
||||
const double gain = active
|
||||
? current_[object][internal] +
|
||||
(static_cast<double>(sample) / static_cast<double>(kBlock)) *
|
||||
step_[object][internal]
|
||||
: fixed_gain;
|
||||
output[(start + sample) * layout_->channel_count + standard] +=
|
||||
static_cast<double>(
|
||||
input[(start + sample) * EJOC_OUTPUT_CHANNELS + object + 1]) * gain;
|
||||
}
|
||||
if (active) {
|
||||
current_[object][internal] += step_[object][internal];
|
||||
--remaining_[object][internal];
|
||||
if (remaining_[object][internal] == 0) {
|
||||
current_[object][internal] = target_[object][internal];
|
||||
}
|
||||
} else {
|
||||
current_[object][internal] = target_[object][internal];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const LayoutGeometry* layout_;
|
||||
double attenuation_db_{};
|
||||
int floor_y_exponent_{};
|
||||
ObjectChannelGains current_{};
|
||||
ObjectChannelGains target_{};
|
||||
ObjectChannelGains step_{};
|
||||
RemainingCounts remaining_{};
|
||||
std::array<std::uint8_t, kChannels> standard_index_for_internal_{};
|
||||
std::array<char, 256> error_{};
|
||||
};
|
||||
|
||||
} // namespace ejoc::speaker
|
||||
|
||||
extern "C" {
|
||||
|
||||
uint32_t EJOC_CALL ejoc_speaker_layout_channel_count(const uint32_t speaker_bitfield) {
|
||||
const auto* layout = ejoc::speaker_tables::find_layout(speaker_bitfield);
|
||||
return layout ? layout->channel_count : 0;
|
||||
}
|
||||
|
||||
ejoc_speaker_renderer_handle EJOC_CALL ejoc_speaker_renderer_create(
|
||||
const uint32_t speaker_bitfield) {
|
||||
const auto* layout = ejoc::speaker_tables::find_layout(speaker_bitfield);
|
||||
if (!layout) {
|
||||
return nullptr;
|
||||
}
|
||||
return new (std::nothrow) ejoc::speaker::Renderer(layout);
|
||||
}
|
||||
|
||||
void EJOC_CALL ejoc_speaker_renderer_destroy(ejoc_speaker_renderer_handle handle) {
|
||||
delete static_cast<ejoc::speaker::Renderer*>(handle);
|
||||
}
|
||||
|
||||
int EJOC_CALL ejoc_speaker_renderer_reset(ejoc_speaker_renderer_handle handle) {
|
||||
if (!handle) {
|
||||
return -1;
|
||||
}
|
||||
return static_cast<ejoc::speaker::Renderer*>(handle)->reset();
|
||||
}
|
||||
|
||||
const char* EJOC_CALL ejoc_speaker_renderer_last_error(ejoc_speaker_renderer_handle handle) {
|
||||
if (!handle) {
|
||||
return "speaker renderer handle is null";
|
||||
}
|
||||
return static_cast<ejoc::speaker::Renderer*>(handle)->last_error();
|
||||
}
|
||||
|
||||
int EJOC_CALL ejoc_speaker_renderer_process(
|
||||
ejoc_speaker_renderer_handle handle,
|
||||
const float* objects16_interleaved,
|
||||
const uint32_t sample_count,
|
||||
const uint32_t metadata_count,
|
||||
const uint32_t* metadata_offsets,
|
||||
const uint32_t* ramp_durations,
|
||||
const uint16_t* positions_q15,
|
||||
const uint8_t* region_indices,
|
||||
const uint8_t* height_enabled,
|
||||
const double* object_gains,
|
||||
double* output_interleaved) {
|
||||
if (!handle) {
|
||||
return -1;
|
||||
}
|
||||
return static_cast<ejoc::speaker::Renderer*>(handle)->process(
|
||||
objects16_interleaved, sample_count, metadata_count,
|
||||
metadata_offsets, ramp_durations, positions_q15,
|
||||
region_indices, height_enabled, object_gains, output_interleaved);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,62 @@
|
||||
#include "speaker/speaker_step.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
namespace joc::speaker {
|
||||
|
||||
Status step(SpeakerStep* context, const std::vector<float>& objects16_planar,
|
||||
const oamd::OamdUpdate* update, std::string* error) {
|
||||
if (context == nullptr || context->handle == nullptr) {
|
||||
return Status::fail(JOC_ERR_STATE, stage::kRender, "speaker renderer is not open");
|
||||
}
|
||||
if (objects16_planar.size() !=
|
||||
static_cast<std::size_t>(JOC_OUTPUT_CHANNELS) * JOC_FRAME_SAMPLES) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kRender,
|
||||
"objects16 must be [16][1536]");
|
||||
}
|
||||
context->interleaved.assign(static_cast<std::size_t>(JOC_FRAME_SAMPLES) * JOC_OUTPUT_CHANNELS,
|
||||
0.0f);
|
||||
for (std::size_t n = 0; n < JOC_FRAME_SAMPLES; ++n) {
|
||||
for (std::size_t c = 0; c < JOC_OUTPUT_CHANNELS; ++c) {
|
||||
context->interleaved[n * JOC_OUTPUT_CHANNELS + c] =
|
||||
objects16_planar[c * JOC_FRAME_SAMPLES + n];
|
||||
}
|
||||
}
|
||||
context->output.assign(
|
||||
static_cast<std::size_t>(JOC_FRAME_SAMPLES) * context->layout.channel_count, 0.0);
|
||||
|
||||
context->last_had_payload = update != nullptr;
|
||||
std::uint32_t ramp = 0;
|
||||
if (update != nullptr) {
|
||||
context->state.apply(*update);
|
||||
ramp = update->ramp_duration_samples;
|
||||
context->last_block_offset = update->block_offset_samples;
|
||||
context->last_ramp_duration = update->ramp_duration_samples;
|
||||
context->last_object_count = update->object_count;
|
||||
}
|
||||
|
||||
std::uint16_t positions[oamd::kObjects][3] = {};
|
||||
context->state.object_positions_q15(positions);
|
||||
// A frame without OAMD must not touch the gains at all (no event), otherwise a
|
||||
// pending ramp would snap - matching the reference exactly.
|
||||
const std::uint32_t event_count = context->last_had_payload ? 1u : 0u;
|
||||
const std::uint32_t offset = context->metadata_offset;
|
||||
const int result = ejoc_speaker_renderer_process(
|
||||
context->handle, context->interleaved.data(), JOC_FRAME_SAMPLES, event_count,
|
||||
event_count != 0u ? &offset : nullptr, event_count != 0u ? &ramp : nullptr,
|
||||
event_count != 0u ? &positions[0][0] : nullptr, nullptr, nullptr, nullptr,
|
||||
context->output.data());
|
||||
if (result != 0) {
|
||||
const char* message = ejoc_speaker_renderer_last_error(context->handle);
|
||||
const std::string text =
|
||||
"ejoc_speaker_renderer_process failed (" + std::to_string(result) + "): " +
|
||||
(message != nullptr ? message : "unknown");
|
||||
if (error != nullptr) {
|
||||
*error = text;
|
||||
}
|
||||
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender, text);
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
} // namespace joc::speaker
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "eac3joc_core.h"
|
||||
#include "foundation/status.h"
|
||||
#include "oamd/oamd_parser.h"
|
||||
#include "speaker/speaker_layout_lookup.h"
|
||||
|
||||
namespace joc::speaker {
|
||||
|
||||
struct SpeakerStep {
|
||||
ejoc_speaker_renderer_handle handle = nullptr;
|
||||
LayoutInfo layout{};
|
||||
oamd::OamdState state;
|
||||
std::uint32_t metadata_offset = 1473;
|
||||
std::vector<float> interleaved;
|
||||
std::vector<double> output;
|
||||
std::uint32_t last_block_offset = 0;
|
||||
std::uint32_t last_ramp_duration = 0;
|
||||
std::uint32_t last_object_count = 0;
|
||||
bool last_had_payload = false;
|
||||
};
|
||||
|
||||
Status step(SpeakerStep* context, const std::vector<float>& objects16_planar,
|
||||
const oamd::OamdUpdate* update, std::string* error);
|
||||
|
||||
} // namespace joc::speaker
|
||||
@@ -0,0 +1,474 @@
|
||||
#include "stream/stream.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
#include "adm/adm_metadata.h"
|
||||
#include "foundation/status.h"
|
||||
#include "hrtf/jochrtf.h"
|
||||
#include "hrtf/rosella_model.h"
|
||||
#include "hrtf/sofa_cache.h"
|
||||
#include "joc_bitstream/joc_parser.h"
|
||||
#include "joc_core/objects16.h"
|
||||
|
||||
namespace joc::stream {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kFrameSamples = JOC_FRAME_SAMPLES;
|
||||
constexpr std::size_t kBedChannels = 6;
|
||||
constexpr int kCoreChannels[5] = {0, 1, 2, 4, 5};
|
||||
constexpr int kLfeChannel = 3;
|
||||
|
||||
} // namespace
|
||||
|
||||
Stream::~Stream() {
|
||||
if (rebuilder_ != nullptr) {
|
||||
ejoc_renderer_destroy(rebuilder_);
|
||||
rebuilder_ = nullptr;
|
||||
}
|
||||
if (speaker_.handle != nullptr) {
|
||||
ejoc_speaker_renderer_destroy(speaker_.handle);
|
||||
speaker_.handle = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Stream::reset_state() {
|
||||
reader_ = eac3::FrameReader();
|
||||
metadata_.clear();
|
||||
bed_pending_.clear();
|
||||
objects16_.clear();
|
||||
output_.clear();
|
||||
read_offset_ = 0;
|
||||
info_ = Info();
|
||||
info_.output_channels = output_channels_;
|
||||
if (rebuilder_ != nullptr) {
|
||||
ejoc_renderer_reset(rebuilder_);
|
||||
}
|
||||
if (speaker_.handle != nullptr) {
|
||||
ejoc_speaker_renderer_reset(speaker_.handle);
|
||||
speaker_.state.reset();
|
||||
speaker_.output.clear();
|
||||
speaker_.last_had_payload = false;
|
||||
}
|
||||
if (binaural_ready_) {
|
||||
binaural_.reset();
|
||||
}
|
||||
if (rosella_ready_) {
|
||||
rosella_.reset();
|
||||
}
|
||||
rosella_pending_.clear();
|
||||
rosella_read_offset_ = 0;
|
||||
}
|
||||
|
||||
Status Stream::create(const Config& config) {
|
||||
if (rebuilder_ != nullptr) {
|
||||
return Status::fail(JOC_ERR_STATE, stage::kRender, "stream already created");
|
||||
}
|
||||
config_ = config;
|
||||
if (config_.input > JOC_STREAM_IN_PCM_OBJECTS16 ||
|
||||
config_.output > JOC_STREAM_OUT_BINAURAL) {
|
||||
return Status::fail(JOC_ERR_INVALID_CONFIG, stage::kRender, "unknown stream kind");
|
||||
}
|
||||
if (config_.input == JOC_STREAM_IN_PCM_OBJECTS16 &&
|
||||
config_.output == JOC_STREAM_OUT_PCM_OBJECTS16) {
|
||||
return Status::fail(JOC_ERR_INVALID_CONFIG, stage::kRender,
|
||||
"objects16 input with objects16 output would do nothing");
|
||||
}
|
||||
gain_ = static_cast<float>(std::pow(10.0, config_.gain_db / 20.0));
|
||||
|
||||
if (config_.input == JOC_STREAM_IN_EAC3) {
|
||||
rebuilder_ = ejoc_renderer_create();
|
||||
if (rebuilder_ == nullptr) {
|
||||
return Status::fail(JOC_ERR_OUT_OF_MEMORY, stage::kDsp, "cannot create the JOC kernel");
|
||||
}
|
||||
if (config_.native_threads != 0u) {
|
||||
ejoc_renderer_set_threads(rebuilder_, config_.native_threads);
|
||||
}
|
||||
}
|
||||
|
||||
if (config_.output == JOC_STREAM_OUT_SPEAKER) {
|
||||
speaker_enabled_ = true;
|
||||
if (!speaker::layout_by_name(config_.layout.c_str(), &speaker_.layout)) {
|
||||
return Status::fail(JOC_ERR_LAYOUT_UNSUPPORTED, stage::kRender,
|
||||
"unknown speaker layout: " + config_.layout);
|
||||
}
|
||||
speaker_.metadata_offset = config_.metadata_offset;
|
||||
speaker_.handle = ejoc_speaker_renderer_create(speaker_.layout.speaker_bitfield);
|
||||
if (speaker_.handle == nullptr) {
|
||||
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender,
|
||||
"cannot create the speaker renderer");
|
||||
}
|
||||
output_channels_ = speaker_.layout.channel_count;
|
||||
} else if (config_.output == JOC_STREAM_OUT_BINAURAL) {
|
||||
binaural_enabled_ = true;
|
||||
// The HRTF input precedence is joc_task_config's: a Rosella
|
||||
// .personalized_headphone wins over a SOFA that the library compiles, and
|
||||
// the compiled .jochrtf (hrtf_path) stays the fallback input.
|
||||
if (!config_.personalized_headphone_path.empty()) {
|
||||
hrtf::RosellaModel model;
|
||||
Status status =
|
||||
hrtf::load_personalized_headphone(config_.personalized_headphone_path, &model);
|
||||
hrtf::RosellaRenderOptions render_options;
|
||||
if (config_.binaural_mode == JOC_BINAURAL_NEAR) {
|
||||
render_options.profile = hrtf::RosellaProfile::Near;
|
||||
} else if (config_.binaural_mode == JOC_BINAURAL_FAR) {
|
||||
render_options.profile = hrtf::RosellaProfile::Far;
|
||||
}
|
||||
render_options.object_delay_samples = config_.object_delay_samples;
|
||||
render_options.tail_seconds = config_.tail_seconds;
|
||||
render_options.output_gain = std::pow(10.0, config_.gain_db / 20.0);
|
||||
if (status.ok()) {
|
||||
status = rosella_.open(model, render_options);
|
||||
}
|
||||
if (!status.ok()) {
|
||||
return Status::fail(status.code(), stage::kRender,
|
||||
"binaural setup failed: " + status.message());
|
||||
}
|
||||
rosella_ready_ = true;
|
||||
output_channels_ = 2;
|
||||
} else {
|
||||
hrtf::Field field;
|
||||
hrtf::Kernels kernels;
|
||||
Status status = Status::success();
|
||||
if (!config_.hrtf_sofa_path.empty()) {
|
||||
// The .jochrtf is an internal cache: the SOFA is the user-facing input.
|
||||
hrtf::SofaFieldRequest request;
|
||||
request.sofa_path = config_.hrtf_sofa_path;
|
||||
request.options.shell_radius_m = config_.hrtf_radius_m;
|
||||
request.cache_dir = config_.hrtf_cache_dir;
|
||||
switch (config_.hrtf_cache_policy) {
|
||||
case JOC_HRTF_CACHE_NONE: request.policy = hrtf::CachePolicy::None; break;
|
||||
case JOC_HRTF_CACHE_DISK: request.policy = hrtf::CachePolicy::Disk; break;
|
||||
default: request.policy = hrtf::CachePolicy::Memory; break;
|
||||
}
|
||||
std::string cache_path;
|
||||
status = hrtf::load_or_compile_sofa_field(request, &field, &cache_path);
|
||||
if (!status.ok()) {
|
||||
return Status::fail(status.code(), stage::kRender,
|
||||
"binaural setup failed: " + status.message());
|
||||
}
|
||||
} else {
|
||||
status = hrtf::load_jochrtf(config_.hrtf_path, &field);
|
||||
}
|
||||
if (status.ok()) {
|
||||
status = config_.kernels_path.empty()
|
||||
? (kernels = hrtf::builtin_kernels(), Status::success())
|
||||
: hrtf::load_kernels(config_.kernels_path, &kernels);
|
||||
}
|
||||
binaural::Profile profile = binaural::Profile::Mid;
|
||||
if (status.ok() && config_.binaural_mode == JOC_BINAURAL_NEAR) {
|
||||
profile = binaural::Profile::Near;
|
||||
} else if (status.ok() && config_.binaural_mode == JOC_BINAURAL_FAR) {
|
||||
profile = binaural::Profile::Far;
|
||||
}
|
||||
if (status.ok()) {
|
||||
status = binaural_.open(field, kernels, profile);
|
||||
}
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
binaural_ready_ = true;
|
||||
output_channels_ = 2;
|
||||
}
|
||||
} else {
|
||||
output_channels_ = JOC_OUTPUT_CHANNELS;
|
||||
}
|
||||
reset_state();
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status Stream::push_eac3(const std::uint8_t* data, std::size_t size, std::size_t* consumed) {
|
||||
if (rebuilder_ == nullptr) {
|
||||
return Status::fail(JOC_ERR_STATE, stage::kEmdf, "stream is not an E-AC-3 input");
|
||||
}
|
||||
if (consumed != nullptr) {
|
||||
*consumed = size;
|
||||
}
|
||||
if (data != nullptr && size != 0u) {
|
||||
reader_.push(data, size);
|
||||
info_.bytes_in += size;
|
||||
}
|
||||
for (;;) {
|
||||
eac3::Frame frame;
|
||||
const eac3::FrameReader::Next state = reader_.next(&frame);
|
||||
if (state == eac3::FrameReader::Next::End) {
|
||||
break;
|
||||
}
|
||||
if (state == eac3::FrameReader::Next::Fail) {
|
||||
return Status::fail(reader_.error(), stage::kEac3, reader_.error_message());
|
||||
}
|
||||
FrameMetadata entry;
|
||||
emdf::Container container;
|
||||
const Status parsed =
|
||||
joc::parse_eac3_frame(frame.data, frame.size, &entry.params, &container, nullptr);
|
||||
if (!parsed.ok()) {
|
||||
return parsed;
|
||||
}
|
||||
if (const emdf::Payload* payload = container.find(emdf::kIdOamd)) {
|
||||
std::vector<std::uint8_t> bytes;
|
||||
const Status extracted =
|
||||
emdf::extract_payload_bytes(frame.data, frame.size, *payload, &bytes);
|
||||
if (!extracted.ok()) {
|
||||
return extracted;
|
||||
}
|
||||
const Status oamd = oamd::parse_id11(bytes.data(), bytes.size(), &entry.update);
|
||||
if (!oamd.ok()) {
|
||||
return oamd;
|
||||
}
|
||||
entry.has_update = true;
|
||||
entry.outer_offset = static_cast<std::int64_t>(payload->sample_offset);
|
||||
}
|
||||
metadata_.push_back(entry);
|
||||
}
|
||||
return process_ready_frames();
|
||||
}
|
||||
|
||||
Status Stream::push_bed(const float* interleaved6, std::size_t samples, std::size_t* consumed) {
|
||||
if (rebuilder_ == nullptr) {
|
||||
return Status::fail(JOC_ERR_STATE, stage::kDsp, "stream is not an E-AC-3 input");
|
||||
}
|
||||
if (consumed != nullptr) {
|
||||
*consumed = samples;
|
||||
}
|
||||
if (interleaved6 != nullptr && samples != 0u) {
|
||||
bed_pending_.insert(bed_pending_.end(), interleaved6,
|
||||
interleaved6 + samples * kBedChannels);
|
||||
}
|
||||
return process_ready_frames();
|
||||
}
|
||||
|
||||
Status Stream::push_objects16(const float* planar16, std::size_t samples, std::size_t* consumed) {
|
||||
if (consumed != nullptr) {
|
||||
*consumed = samples;
|
||||
}
|
||||
if (planar16 == nullptr || samples == 0u) {
|
||||
return Status::success();
|
||||
}
|
||||
// Rendered immediately: the host has already done the JOC rebuild.
|
||||
for (std::size_t offset = 0; offset < samples; offset += kFrameSamples) {
|
||||
const std::size_t count = std::min(kFrameSamples, samples - offset);
|
||||
std::vector<float> frame(static_cast<std::size_t>(JOC_OUTPUT_CHANNELS) * kFrameSamples,
|
||||
0.0f);
|
||||
for (std::size_t channel = 0; channel < JOC_OUTPUT_CHANNELS; ++channel) {
|
||||
std::memcpy(frame.data() + channel * kFrameSamples,
|
||||
planar16 + channel * samples + offset, count * sizeof(float));
|
||||
}
|
||||
++info_.frames_in;
|
||||
info_.samples_in += count;
|
||||
const Status rendered = render_objects16(frame);
|
||||
if (!rendered.ok()) {
|
||||
return rendered;
|
||||
}
|
||||
if (count != kFrameSamples) {
|
||||
break; // a partial frame is dropped; the host should push whole frames
|
||||
}
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status Stream::process_ready_frames() {
|
||||
while (bed_pending_.size() / kBedChannels >= kFrameSamples && !metadata_.empty()) {
|
||||
const FrameMetadata entry = metadata_.front();
|
||||
metadata_.pop_front();
|
||||
|
||||
std::vector<float> bed5(static_cast<std::size_t>(JOC_CORE_CHANNELS) * kFrameSamples, 0.0f);
|
||||
std::vector<float> lfe(kFrameSamples, 0.0f);
|
||||
for (std::size_t sample = 0; sample < kFrameSamples; ++sample) {
|
||||
for (std::size_t channel = 0; channel < JOC_CORE_CHANNELS; ++channel) {
|
||||
bed5[channel * kFrameSamples + sample] =
|
||||
bed_pending_[sample * kBedChannels + kCoreChannels[channel]];
|
||||
}
|
||||
lfe[sample] = bed_pending_[sample * kBedChannels + kLfeChannel];
|
||||
}
|
||||
bed_pending_.erase(bed_pending_.begin(),
|
||||
bed_pending_.begin() + static_cast<std::ptrdiff_t>(kFrameSamples *
|
||||
kBedChannels));
|
||||
|
||||
std::string error;
|
||||
const Status rebuilt = joc::rebuild_objects16(rebuilder_, entry.params, bed5.data(),
|
||||
lfe.data(), gain_, &objects16_, &error);
|
||||
if (!rebuilt.ok()) {
|
||||
return Status::fail(rebuilt.code(), stage::kDsp, error);
|
||||
}
|
||||
pending_metadata_ = entry;
|
||||
const Status rendered = render_objects16(objects16_);
|
||||
if (!rendered.ok()) {
|
||||
return rendered;
|
||||
}
|
||||
++info_.frames_in;
|
||||
info_.samples_in += kFrameSamples;
|
||||
}
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status Stream::render_objects16(const std::vector<float>& objects16) {
|
||||
if (config_.output == JOC_STREAM_OUT_PCM_OBJECTS16) {
|
||||
output_.insert(output_.end(), objects16.begin(), objects16.end());
|
||||
info_.frames_out++;
|
||||
info_.samples_out += kFrameSamples;
|
||||
return Status::success();
|
||||
}
|
||||
if (speaker_enabled_) {
|
||||
std::string error;
|
||||
const Status stepped =
|
||||
speaker::step(&speaker_, objects16, pending_metadata_.has_update
|
||||
? &pending_metadata_.update
|
||||
: nullptr,
|
||||
&error);
|
||||
if (!stepped.ok()) {
|
||||
return Status::fail(stepped.code(), stage::kRender, error);
|
||||
}
|
||||
for (const double value : speaker_.output) {
|
||||
output_.push_back(static_cast<float>(value));
|
||||
}
|
||||
info_.frames_out++;
|
||||
info_.samples_out += kFrameSamples;
|
||||
return Status::success();
|
||||
}
|
||||
if (rosella_ready_) {
|
||||
return render_rosella_objects16(objects16);
|
||||
}
|
||||
|
||||
const Status submitted =
|
||||
binaural_.submit_frame(objects16.data(),
|
||||
pending_metadata_.has_update ? &pending_metadata_.update : nullptr,
|
||||
static_cast<std::int64_t>(info_.frames_out),
|
||||
pending_metadata_.outer_offset,
|
||||
static_cast<std::int64_t>(config_.object_delay_samples));
|
||||
if (!submitted.ok()) {
|
||||
return submitted;
|
||||
}
|
||||
std::vector<double> produced;
|
||||
binaural_.take_output(&produced);
|
||||
for (const double value : produced) {
|
||||
output_.push_back(static_cast<float>(value));
|
||||
}
|
||||
info_.frames_out++;
|
||||
info_.samples_out += produced.size() / 2u;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
// The Rosella runtime is driven exactly like the SOFA runtime (the same frame,
|
||||
// update, frame index, outer offset and object delay), but it renders in chunks
|
||||
// of its own size (64 frames by default), so a frame usually yields either
|
||||
// nothing or a whole chunk. Its output therefore waits in a FIFO and is released
|
||||
// one frame's worth at a time, which keeps the stream's contract intact: pushing
|
||||
// one syncframe leaves exactly JOC_FRAME_SAMPLES samples for the caller to pull,
|
||||
// and nothing is dropped or counted twice. pull() and flush() release the rest.
|
||||
Status Stream::render_rosella_objects16(const std::vector<float>& objects16) {
|
||||
const Status submitted =
|
||||
rosella_.submit_frame(objects16.data(),
|
||||
pending_metadata_.has_update ? &pending_metadata_.update : nullptr,
|
||||
static_cast<std::int64_t>(info_.frames_out),
|
||||
pending_metadata_.outer_offset,
|
||||
static_cast<std::int64_t>(config_.object_delay_samples));
|
||||
if (!submitted.ok()) {
|
||||
return submitted;
|
||||
}
|
||||
std::vector<double> produced;
|
||||
rosella_.take_output(&produced);
|
||||
if (!produced.empty()) {
|
||||
rosella_pending_.insert(rosella_pending_.end(), produced.begin(), produced.end());
|
||||
}
|
||||
release_rosella_output(kFrameSamples);
|
||||
info_.frames_out++;
|
||||
// Counted as the runtime produces it, which is also how the SOFA path counts:
|
||||
// the totals are identical, only the frame they appear on differs.
|
||||
info_.samples_out += produced.size() / 2u;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
void Stream::release_rosella_output(std::size_t limit) {
|
||||
if (!rosella_ready_ || limit == 0u) {
|
||||
return;
|
||||
}
|
||||
const std::size_t count = std::min(limit, rosella_pending_samples());
|
||||
if (count == 0u) {
|
||||
return;
|
||||
}
|
||||
const std::size_t values = count * 2u;
|
||||
for (std::size_t index = 0; index < values; ++index) {
|
||||
output_.push_back(static_cast<float>(rosella_pending_[rosella_read_offset_ + index]));
|
||||
}
|
||||
rosella_read_offset_ += values;
|
||||
if (rosella_read_offset_ == rosella_pending_.size()) {
|
||||
rosella_pending_.clear();
|
||||
rosella_read_offset_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Status Stream::pull(float* destination, std::size_t capacity_samples, std::size_t* produced) {
|
||||
if (produced != nullptr) {
|
||||
*produced = 0;
|
||||
}
|
||||
if (destination == nullptr || produced == nullptr) {
|
||||
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOutput, "null pull buffer");
|
||||
}
|
||||
// Rendered Rosella samples that the frame-at-a-time release above has not
|
||||
// handed over yet are still the caller's to take; releasing them here keeps
|
||||
// buffered_samples() and the amount pull() can deliver the same number.
|
||||
release_rosella_output(capacity_samples);
|
||||
const std::size_t available = buffered_samples();
|
||||
const std::size_t count = std::min(capacity_samples, available);
|
||||
if (count != 0u) {
|
||||
std::memcpy(destination, output_.data() + read_offset_,
|
||||
count * output_channels_ * sizeof(float));
|
||||
read_offset_ += count * output_channels_;
|
||||
if (read_offset_ == output_.size()) {
|
||||
output_.clear();
|
||||
read_offset_ = 0;
|
||||
} else if (read_offset_ > (1u << 20)) {
|
||||
output_.erase(output_.begin(),
|
||||
output_.begin() + static_cast<std::ptrdiff_t>(read_offset_));
|
||||
read_offset_ = 0;
|
||||
}
|
||||
}
|
||||
*produced = static_cast<std::uint32_t>(count);
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status Stream::flush() {
|
||||
if (binaural_ready_) {
|
||||
std::vector<double> tail;
|
||||
const Status drained =
|
||||
binaural_.finish(binaural_.finish_capacity(config_.tail_seconds), &tail);
|
||||
if (!drained.ok()) {
|
||||
return drained;
|
||||
}
|
||||
for (const double value : tail) {
|
||||
output_.push_back(static_cast<float>(value));
|
||||
}
|
||||
info_.samples_out += tail.size() / 2u;
|
||||
}
|
||||
if (rosella_ready_) {
|
||||
std::vector<double> tail;
|
||||
const Status drained =
|
||||
rosella_.finish(rosella_.finish_capacity(config_.tail_seconds), &tail);
|
||||
if (!drained.ok()) {
|
||||
return drained;
|
||||
}
|
||||
// Everything the runtime produced as the program is released first: the
|
||||
// tail only sounds after it. The program samples were already counted by
|
||||
// render_rosella_objects16, so only the tail is added here.
|
||||
release_rosella_output(rosella_pending_samples());
|
||||
for (const double value : tail) {
|
||||
output_.push_back(static_cast<float>(value));
|
||||
}
|
||||
info_.samples_out += tail.size() / 2u;
|
||||
}
|
||||
info_.ended = 1;
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
Status Stream::reset() {
|
||||
if (rebuilder_ == nullptr && !speaker_enabled_ && !binaural_ready_ && !rosella_ready_ &&
|
||||
config_.input != JOC_STREAM_IN_PCM_OBJECTS16) {
|
||||
return Status::fail(JOC_ERR_STATE, stage::kRender, "stream is not created");
|
||||
}
|
||||
reset_state();
|
||||
return Status::success();
|
||||
}
|
||||
|
||||
} // namespace joc::stream
|
||||
@@ -0,0 +1,125 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "binaural/binaural_runtime.h"
|
||||
#include "eac3_transport/eac3_reader.h"
|
||||
#include "foundation/status.h"
|
||||
#include "hrtf/rosella_renderer.h"
|
||||
#include "joc_core.h"
|
||||
#include "joc_stream.h"
|
||||
#include "oamd/oamd_parser.h"
|
||||
#include "speaker/speaker_step.h"
|
||||
|
||||
namespace joc::stream {
|
||||
|
||||
struct Config {
|
||||
std::uint32_t input = JOC_STREAM_IN_EAC3;
|
||||
std::uint32_t output = JOC_STREAM_OUT_PCM_OBJECTS16;
|
||||
std::string layout;
|
||||
std::uint32_t metadata_offset = 1473;
|
||||
std::uint32_t binaural_mode = JOC_BINAURAL_MID;
|
||||
std::string hrtf_path;
|
||||
std::string kernels_path;
|
||||
double tail_seconds = 5.0;
|
||||
std::uint32_t object_delay_samples = 1473;
|
||||
double gain_db = 0.0;
|
||||
std::uint32_t native_threads = 0;
|
||||
// The three HRTF shapes of joc_task_config, in its precedence order: a Rosella
|
||||
// model wins over a SOFA, and hrtf_path (.jochrtf) is the fallback.
|
||||
std::string hrtf_sofa_path;
|
||||
std::string personalized_headphone_path;
|
||||
std::string hrtf_cache_dir;
|
||||
std::uint32_t hrtf_cache_policy = JOC_HRTF_CACHE_MEMORY;
|
||||
double hrtf_radius_m = 1.0;
|
||||
};
|
||||
|
||||
struct Info {
|
||||
std::uint64_t frames_in = 0;
|
||||
std::uint64_t frames_out = 0;
|
||||
std::uint64_t samples_in = 0;
|
||||
std::uint64_t samples_out = 0;
|
||||
std::uint64_t bytes_in = 0;
|
||||
std::uint64_t oamd_payloads = 0;
|
||||
std::uint64_t oamd_transitions = 0;
|
||||
std::uint32_t output_channels = 0;
|
||||
std::uint32_t ended = 0;
|
||||
};
|
||||
|
||||
// One frame's metadata, queued while the matching core PCM arrives.
|
||||
struct FrameMetadata {
|
||||
joc_frame_params params{};
|
||||
oamd::OamdUpdate update{};
|
||||
bool has_update = false;
|
||||
std::int64_t outer_offset = 0;
|
||||
};
|
||||
|
||||
class Stream {
|
||||
public:
|
||||
Stream() = default;
|
||||
~Stream();
|
||||
|
||||
Stream(const Stream&) = delete;
|
||||
Stream& operator=(const Stream&) = delete;
|
||||
|
||||
Status create(const Config& config);
|
||||
Status push_eac3(const std::uint8_t* data, std::size_t size, std::size_t* consumed);
|
||||
Status push_bed(const float* interleaved6, std::size_t samples, std::size_t* consumed);
|
||||
Status push_objects16(const float* planar16, std::size_t samples, std::size_t* consumed);
|
||||
Status pull(float* destination, std::size_t capacity_samples, std::size_t* produced);
|
||||
Status flush();
|
||||
Status reset();
|
||||
|
||||
const Info& info() const { return info_; }
|
||||
// Per-channel sample count, not the interleaved float count. The Rosella
|
||||
// runtime renders in its own chunk size, so its output waits in a FIFO before
|
||||
// it is released one frame at a time; those samples are rendered and unpulled
|
||||
// as well, so the reported backlog has to include them.
|
||||
std::size_t buffered_samples() const {
|
||||
return output_channels_ != 0u
|
||||
? (output_.size() - read_offset_) / output_channels_ + rosella_pending_samples()
|
||||
: 0u;
|
||||
}
|
||||
|
||||
private:
|
||||
Status process_ready_frames();
|
||||
Status render_objects16(const std::vector<float>& objects16);
|
||||
Status render_rosella_objects16(const std::vector<float>& objects16);
|
||||
// Moves at most `limit` rendered stereo samples per channel out of the FIFO
|
||||
// into output_, oldest sample first.
|
||||
void release_rosella_output(std::size_t limit);
|
||||
std::size_t rosella_pending_samples() const {
|
||||
return rosella_ready_ ? (rosella_pending_.size() - rosella_read_offset_) / 2u : 0u;
|
||||
}
|
||||
void reset_state();
|
||||
|
||||
Config config_;
|
||||
Info info_;
|
||||
eac3::FrameReader reader_;
|
||||
std::deque<FrameMetadata> metadata_;
|
||||
FrameMetadata pending_metadata_;
|
||||
std::vector<float> bed_pending_;
|
||||
std::vector<std::uint8_t> frame_copy_;
|
||||
std::vector<float> objects16_;
|
||||
std::vector<float> output_;
|
||||
std::size_t read_offset_ = 0;
|
||||
std::uint32_t output_channels_ = 0;
|
||||
|
||||
ejoc_renderer_handle rebuilder_ = nullptr;
|
||||
speaker::SpeakerStep speaker_;
|
||||
binaural::SofaBinauralRuntime binaural_;
|
||||
hrtf::RosellaRuntime rosella_;
|
||||
std::vector<double> rosella_pending_;
|
||||
std::size_t rosella_read_offset_ = 0;
|
||||
bool speaker_enabled_ = false;
|
||||
bool binaural_enabled_ = false;
|
||||
bool binaural_ready_ = false;
|
||||
bool rosella_ready_ = false;
|
||||
float gain_ = 1.0f;
|
||||
};
|
||||
|
||||
} // namespace joc::stream
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "foundation/status.h"
|
||||
#include "joc_core.h"
|
||||
|
||||
namespace joc::task {
|
||||
|
||||
Status validate(const joc_task_config& raw, std::vector<joc_validation_issue>* issues,
|
||||
std::uint32_t* error_count);
|
||||
|
||||
// Runs the task on the calling thread. Never throws; failures come back as the
|
||||
Status run(const joc_task_config& raw, const joc_event_sink* sink, joc_task_result* out);
|
||||
|
||||
// Serialises the stable subset of the result (plan 22.2 / 33.5).
|
||||
Status result_to_json(const joc_task_result& result, std::string* out);
|
||||
|
||||
} // namespace joc::task
|
||||
@@ -0,0 +1,111 @@
|
||||
#include "telemetry/event_bus.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <type_traits>
|
||||
|
||||
namespace joc::telemetry {
|
||||
|
||||
// The event must stay a trivially copyable POD with no pointers: that is what
|
||||
// makes "an event can never carry audio" a compile-time property.
|
||||
static_assert(std::is_trivially_copyable_v<joc_event>, "joc_event must be a POD");
|
||||
static_assert(sizeof(joc_event) <= 512, "joc_event must stay small");
|
||||
|
||||
const char* stage_name(joc_stage stage) {
|
||||
switch (stage) {
|
||||
case JOC_STAGE_IDLE: return "idle";
|
||||
case JOC_STAGE_INPUT: return "input";
|
||||
case JOC_STAGE_METADATA: return "metadata";
|
||||
case JOC_STAGE_DECODE: return "decode";
|
||||
case JOC_STAGE_JOC: return "joc";
|
||||
case JOC_STAGE_RENDER: return "render";
|
||||
case JOC_STAGE_OUTPUT: return "output";
|
||||
case JOC_STAGE_DONE: return "done";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
EventBus::EventBus(const joc_event_sink* sink) {
|
||||
if (sink != nullptr && sink->callback != nullptr) {
|
||||
sink_ = *sink;
|
||||
has_sink_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void EventBus::set_totals(std::uint64_t total_frames, std::uint64_t total_samples) {
|
||||
total_frames_ = total_frames;
|
||||
total_samples_ = total_samples;
|
||||
}
|
||||
|
||||
double EventBus::elapsed_seconds() const {
|
||||
return std::chrono::duration<double>(std::chrono::steady_clock::now() - started_).count();
|
||||
}
|
||||
|
||||
void EventBus::publish(joc_event* event) {
|
||||
if (!has_sink_) {
|
||||
return;
|
||||
}
|
||||
if (sink_.min_type != 0u && event->type < sink_.min_type) {
|
||||
return;
|
||||
}
|
||||
if (sink_.max_type != 0u && event->type > sink_.max_type) {
|
||||
return;
|
||||
}
|
||||
sink_.callback(sink_.user, event);
|
||||
}
|
||||
|
||||
void EventBus::emit(std::uint32_t type, joc_stage stage, std::uint32_t log_level,
|
||||
const std::string& message, joc_error code) {
|
||||
joc_event event{};
|
||||
event.struct_size = sizeof(joc_event);
|
||||
event.type = type;
|
||||
event.sequence = ++sequence_;
|
||||
event.timestamp_us = static_cast<std::uint64_t>(elapsed_seconds() * 1e6);
|
||||
event.total_frames = total_frames_;
|
||||
event.total_samples = total_samples_;
|
||||
event.stage = static_cast<std::uint32_t>(stage);
|
||||
event.backend = backend_;
|
||||
event.progress = total_frames_ != 0u
|
||||
? static_cast<double>(event.current_frame) /
|
||||
static_cast<double>(total_frames_)
|
||||
: -1.0;
|
||||
event.elapsed_seconds = elapsed_seconds();
|
||||
event.error_code = code;
|
||||
event.log_level = log_level;
|
||||
std::snprintf(event.stage_name, sizeof(event.stage_name), "%s", stage_name(stage));
|
||||
std::snprintf(event.message, sizeof(event.message), "%s", message.c_str());
|
||||
publish(&event);
|
||||
}
|
||||
|
||||
void EventBus::progress(std::uint64_t frame, std::uint64_t sample, std::uint64_t output_samples,
|
||||
std::uint64_t output_bytes, double output_seconds) {
|
||||
joc_event event{};
|
||||
event.struct_size = sizeof(joc_event);
|
||||
event.type = JOC_EV_PROGRESS;
|
||||
event.sequence = ++sequence_;
|
||||
event.timestamp_us = static_cast<std::uint64_t>(elapsed_seconds() * 1e6);
|
||||
event.current_frame = frame;
|
||||
event.total_frames = total_frames_;
|
||||
event.current_sample = sample;
|
||||
event.total_samples = total_samples_;
|
||||
event.stage = static_cast<std::uint32_t>(stage_);
|
||||
event.backend = backend_;
|
||||
event.progress = total_frames_ != 0u
|
||||
? static_cast<double>(frame) / static_cast<double>(total_frames_)
|
||||
: -1.0;
|
||||
event.elapsed_seconds = elapsed_seconds();
|
||||
const double audio_seconds = static_cast<double>(sample) / 48000.0;
|
||||
event.realtime_factor = event.elapsed_seconds > 0.0 ? audio_seconds / event.elapsed_seconds
|
||||
: 0.0;
|
||||
event.output_samples = output_samples;
|
||||
event.output_bytes = output_bytes;
|
||||
event.output_duration_seconds = output_seconds;
|
||||
std::snprintf(event.stage_name, sizeof(event.stage_name), "%s", stage_name(stage_));
|
||||
publish(&event);
|
||||
}
|
||||
|
||||
void EventBus::stage(joc_stage stage, const std::string& message) {
|
||||
stage_ = stage;
|
||||
emit(JOC_EV_STAGE_CHANGED, stage, JOC_LOG_INFO, message);
|
||||
}
|
||||
|
||||
} // namespace joc::telemetry
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "joc_core.h"
|
||||
|
||||
namespace joc::telemetry {
|
||||
|
||||
const char* stage_name(joc_stage stage);
|
||||
|
||||
class EventBus {
|
||||
public:
|
||||
EventBus() = default;
|
||||
explicit EventBus(const joc_event_sink* sink);
|
||||
|
||||
void set_totals(std::uint64_t total_frames, std::uint64_t total_samples);
|
||||
void set_backend(std::uint32_t backend) { backend_ = backend; }
|
||||
|
||||
// Generic emit; `message` is truncated into the 256-byte field.
|
||||
void emit(std::uint32_t type, joc_stage stage, std::uint32_t log_level, const std::string& message,
|
||||
joc_error code = JOC_OK);
|
||||
|
||||
void progress(std::uint64_t frame, std::uint64_t sample, std::uint64_t output_samples, std::uint64_t output_bytes,
|
||||
double output_seconds);
|
||||
void stage(joc_stage stage, const std::string& message = std::string());
|
||||
void info(const std::string& message) { emit(JOC_EV_LOG, stage_, JOC_LOG_INFO, message); }
|
||||
void warning(const std::string& message)
|
||||
{
|
||||
++warning_count_;
|
||||
emit(JOC_EV_WARNING, stage_, JOC_LOG_WARNING, message);
|
||||
}
|
||||
void error(joc_error code, const std::string& stage_text, const std::string& message)
|
||||
{
|
||||
++error_count_;
|
||||
emit(JOC_EV_ERROR, stage_, JOC_LOG_ERROR,
|
||||
(stage_text.empty() ? message : stage_text + ": " + message), code);
|
||||
}
|
||||
|
||||
std::uint64_t sequence() const { return sequence_; }
|
||||
std::uint32_t warning_count() const { return warning_count_; }
|
||||
std::uint32_t error_count() const { return error_count_; }
|
||||
double elapsed_seconds() const;
|
||||
|
||||
private:
|
||||
void publish(joc_event* event);
|
||||
|
||||
joc_event_sink sink_{};
|
||||
bool has_sink_ = false;
|
||||
std::chrono::steady_clock::time_point started_ = std::chrono::steady_clock::now();
|
||||
std::uint64_t sequence_ = 0;
|
||||
std::uint64_t total_frames_ = 0;
|
||||
std::uint64_t total_samples_ = 0;
|
||||
std::uint32_t backend_ = 0;
|
||||
std::uint32_t warning_count_ = 0;
|
||||
std::uint32_t error_count_ = 0;
|
||||
joc_stage stage_ = JOC_STAGE_IDLE;
|
||||
};
|
||||
|
||||
} // namespace joc::telemetry
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user