Init foo_input_joc
build / windows (push) Has been cancelled

This commit is contained in:
2026-09-25 17:07:04 +08:00
commit ad62a914e6
127 changed files with 38545 additions and 0 deletions
+43
View File
@@ -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
View File
@@ -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
+21
View File
@@ -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.
+114
View File
@@ -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).
+62
View File
@@ -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
View File
@@ -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.
+157
View File
@@ -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>
+239
View File
@@ -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
+490
View File
@@ -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
+136
View File
@@ -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
+257
View File
@@ -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>
+333
View File
@@ -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
+43
View File
@@ -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
+281
View File
@@ -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
+61
View File
@@ -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
+249
View File
@@ -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"
+162
View File
@@ -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"
+120
View File
@@ -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"
+664
View File
@@ -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"
+251
View File
@@ -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
+96
View File
@@ -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
+795
View File
@@ -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;
}
+113
View File
@@ -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
+68
View File
@@ -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
+305
View File
@@ -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
+57
View File
@@ -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
+30
View File
@@ -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
+125
View File
@@ -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
+206
View File
@@ -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
+68
View File
@@ -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
+164
View File
@@ -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
+47
View File
@@ -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
+25
View File
@@ -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
+227
View File
@@ -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
+25
View File
@@ -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
+25
View File
@@ -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
+165
View File
@@ -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
+31
View File
@@ -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
+48
View File
@@ -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
+401
View File
@@ -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", &centers, {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, &centers, &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
+66
View File
@@ -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
+610
View File
@@ -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
+55
View File
@@ -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
+535
View File
@@ -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, &parameters, &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
+88
View File
@@ -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
+64
View File
@@ -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
+255
View File
@@ -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
+61
View File
@@ -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
+237
View File
@@ -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
+38
View File
@@ -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
+103
View File
@@ -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
+241
View File
@@ -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
+52
View File
@@ -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
+87
View File
@@ -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
+276
View File
@@ -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
+12
View File
@@ -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
+420
View File
@@ -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
+42
View File
@@ -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
+263
View File
@@ -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
+39
View File
@@ -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
+186
View File
@@ -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
+19
View File
@@ -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
+242
View File
@@ -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
+61
View File
@@ -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
+215
View File
@@ -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
+38
View File
@@ -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}
};
+415
View File
@@ -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, &params, &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
+46
View File
@@ -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
+735
View File
@@ -0,0 +1,735 @@
#define EJOC_BUILD_DLL
#include "eac3joc_core.h"
#include "qmf_tables.h"
#include <algorithm>
#include <atomic>
#include <barrier>
#include <cmath>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <memory>
#include <new>
#include <thread>
#include <vector>
namespace ejoc {
struct Complex {
double re;
double im;
};
inline Complex mul(const Complex a, const Complex b) noexcept {
return {a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re};
}
inline double clamp_unit(double value) noexcept {
return value < -1.0 ? -1.0 : (value > 1.0 ? 1.0 : value);
}
constexpr double kPi = 3.141592653589793238462643383279502884;
constexpr int kLfeDelay = 1217;
constexpr int kMaxBands = EJOC_MAX_PARAMETER_BANDS;
const uint8_t* parameter_band_map(const int bands) noexcept {
using namespace tables;
switch (bands) {
case 1: return kPbMap1;
case 3: return kPbMap3;
case 5: return kPbMap5;
case 7: return kPbMap7;
case 9: return kPbMap9;
case 12: return kPbMap12;
case 15: return kPbMap15;
case 23: return kPbMap23;
default: return nullptr;
}
}
class Renderer final {
public:
Renderer() noexcept {
initialize_tables();
reset();
}
~Renderer() noexcept {
stop_workers();
}
int reset() noexcept {
std::memset(analysis_fifo_, 0, sizeof(analysis_fifo_));
std::memset(analysis_delay_, 0, sizeof(analysis_delay_));
std::memset(surround_delay_, 0, sizeof(surround_delay_));
std::memset(surround_history_, 0, sizeof(surround_history_));
std::memset(lfe_delay_, 0, sizeof(lfe_delay_));
std::memset(matrix_previous_, 0, sizeof(matrix_previous_));
std::memset(synthesis_state_, 0, sizeof(synthesis_state_));
std::memset(x_, 0, sizeof(x_));
std::memset(z_, 0, sizeof(z_));
analysis_phase_ = 0.0625f;
error_[0] = '\0';
return 0;
}
int set_threads(uint32_t total_threads) noexcept {
if (total_threads < 1) {
total_threads = 1;
}
if (total_threads > EJOC_MAX_OBJECTS) {
total_threads = EJOC_MAX_OBJECTS;
}
stop_workers();
job_count_ = 0;
next_job_.store(0, std::memory_order_relaxed);
if (total_threads == 1) {
return 0;
}
try {
stop_.store(false, std::memory_order_relaxed);
pool_ready_.store(false, std::memory_order_relaxed);
work_barrier_ = std::make_unique<std::barrier<>>(static_cast<std::ptrdiff_t>(total_threads));
workers_.reserve(total_threads - 1);
for (uint32_t index = 1; index < total_threads; ++index) {
workers_.emplace_back([this]() noexcept { worker_loop(); });
}
pool_ready_.store(true, std::memory_order_release);
// Startup rendezvous: ensure every worker has entered the two-phase
// barrier loop before set_threads returns, so an immediate destroy
// cannot race a worker that exits before reaching the barrier.
work_barrier_->arrive_and_wait();
work_barrier_->arrive_and_wait();
} catch (...) {
stop_.store(true, std::memory_order_release);
pool_ready_.store(true, std::memory_order_release);
for (std::thread& worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
workers_.clear();
work_barrier_.reset();
stop_.store(false, std::memory_order_relaxed);
return fail("failed to create native worker threads");
}
return 0;
}
uint32_t thread_count() const noexcept {
return static_cast<uint32_t>(workers_.size() + 1);
}
const char* error() const noexcept {
return error_[0] ? error_ : "";
}
int process(
const float* bed5,
const float* lfe,
const uint32_t object_mask,
const uint8_t* n_bands,
const uint8_t* n_dpoints,
const uint8_t* slope_idx,
const uint8_t* offset_ts,
const double* dq,
const double clipgain,
const float phase_new,
const float output_scale,
float* output16) noexcept {
error_[0] = '\0';
if (!bed5 || !n_bands || !n_dpoints || !slope_idx || !offset_ts || !dq || !output16) {
return fail("null pointer passed to ejoc_renderer_process");
}
if (object_mask & ~((1u << EJOC_MAX_OBJECTS) - 1u)) {
return fail("object_mask contains an object index above 14");
}
if (!std::isfinite(clipgain) || !std::isfinite(phase_new) || !std::isfinite(output_scale)) {
return fail("clipgain, phase_new, and output_scale must be finite");
}
for (int object = 0; object < EJOC_MAX_OBJECTS; ++object) {
if ((object_mask & (1u << object)) == 0) {
continue;
}
if (!parameter_band_map(n_bands[object])) {
return fail("unsupported parameter-band count");
}
if (n_dpoints[object] < 1 || n_dpoints[object] > 2) {
return fail("n_dpoints must be 1 or 2");
}
if (slope_idx[object] > 1) {
return fail("slope_idx must be 0 or 1");
}
}
std::memset(output16, 0, sizeof(float) * EJOC_OUTPUT_CHANNELS * EJOC_FRAME_SAMPLES);
analysis(bed5, phase_new);
render_lfe(lfe, output_scale, output16);
process_objects(object_mask, n_bands, n_dpoints, slope_idx, offset_ts,
dq, clipgain, output_scale, output16);
return 0;
}
private:
void initialize_tables() noexcept {
for (int i = 0; i < 64; ++i) {
int value = i;
int reversed = 0;
for (int bit = 0; bit < 6; ++bit) {
reversed = (reversed << 1) | (value & 1);
value >>= 1;
}
bit_reverse_[i] = static_cast<uint8_t>(reversed);
const double theta = kPi * static_cast<double>(i) / 128.0;
rotation_sin_[i] = 0.5 * std::sin(theta);
rotation_cos_[i] = 0.5 * std::cos(theta);
}
for (int i = 0; i < 32; ++i) {
const double angle = -2.0 * kPi * static_cast<double>(i) / 64.0;
fft_twiddle_[i] = {std::cos(angle), std::sin(angle)};
}
}
int fail(const char* message) noexcept {
std::snprintf(error_, sizeof(error_), "%s", message);
return -1;
}
void fft64(Complex* values) const noexcept {
for (int i = 0; i < 64; ++i) {
const int j = bit_reverse_[i];
if (j > i) {
const Complex temp = values[i];
values[i] = values[j];
values[j] = temp;
}
}
for (int length = 2; length <= 64; length <<= 1) {
const int half = length >> 1;
const int twiddle_step = 64 / length;
for (int base = 0; base < 64; base += length) {
for (int j = 0; j < half; ++j) {
const Complex even = values[base + j];
const Complex odd = mul(values[base + j + half], fft_twiddle_[j * twiddle_step]);
values[base + j] = {even.re + odd.re, even.im + odd.im};
values[base + j + half] = {even.re - odd.re, even.im - odd.im};
}
}
}
}
void analysis_slot(const int channel, const float* ring, const int timeslot) noexcept {
double v36[64];
double v40[64];
Complex frequency[64];
for (int sample = 0; sample < 64; ++sample) {
v36[sample] =
analysis_fifo_[channel][0][sample] * tables::kAnalysisWindow[8 * 64 + sample] +
analysis_fifo_[channel][2][sample] * tables::kAnalysisWindow[6 * 64 + sample] +
analysis_fifo_[channel][4][sample] * tables::kAnalysisWindow[4 * 64 + sample] +
analysis_fifo_[channel][6][sample] * tables::kAnalysisWindow[2 * 64 + sample] +
analysis_fifo_[channel][8][sample] * tables::kAnalysisWindow[0 * 64 + sample];
v40[sample] =
analysis_fifo_[channel][1][sample] * tables::kAnalysisWindow[7 * 64 + sample] +
analysis_fifo_[channel][3][sample] * tables::kAnalysisWindow[5 * 64 + sample] +
analysis_fifo_[channel][5][sample] * tables::kAnalysisWindow[3 * 64 + sample] +
analysis_fifo_[channel][7][sample] * tables::kAnalysisWindow[1 * 64 + sample] +
static_cast<double>(ring[sample]) * tables::kAnalysisWindow[9 * 64 + sample];
}
for (int k = 0; k < 64; ++k) {
const int source = 63 - k;
const double re = v40[source];
const double im = v36[source];
const double a = rotation_sin_[k];
const double b = rotation_cos_[k];
frequency[k] = {im * a - re * b, im * b + re * a};
}
fft64(frequency);
constexpr double scale = 1.0 / 64.0;
for (int k = 0; k < 32; ++k) {
x_[channel][2 * k][timeslot] = {frequency[k].re * scale, -frequency[k].im * scale};
x_[channel][2 * k + 1][timeslot] = {
frequency[63 - k].re * scale,
frequency[63 - k].im * scale};
}
for (int history = 8; history > 0; --history) {
std::memcpy(analysis_fifo_[channel][history], analysis_fifo_[channel][history - 1],
sizeof(analysis_fifo_[channel][history]));
}
for (int sample = 0; sample < 64; ++sample) {
analysis_fifo_[channel][0][sample] = static_cast<double>(ring[sample]);
}
}
void surround_post() noexcept {
static constexpr double kDcA[21] = {
-.0006242550443857908, -.0019234686624258757, -.0042654648423194885,
-.008168308064341545, -.014327201060950756, -.023759860545396805,
-.03757232800126076, -.05577569454908371, -.07568276673555374,
-.09172472357749939, -.5979374051094055, -.09172472357749939,
-.07568276673555374, -.05577569454908371, -.03757232800126076,
-.023759860545396805, -.014327201060950756, -.008168308064341545,
-.0042654648423194885, -.0019234686624258757, -.0006242550443857908,
};
static constexpr double kDcB[21] = {
.0013996040215715766, .003839150769636035, .007512642536312342,
.012419373728334904, .018367428332567215, .0249701626598835,
.03167900815606117, .03785000368952751, .04283412545919418,
.04607561603188515, .047200120985507965, .04607561603188515,
.04283412545919418, .03785000368952751, .03167900815606117,
.0249701626598835, .018367428332567215, .012419373728334904,
.007512642536312342, .003839150769636035, .0013996040215715766,
};
for (int surround = 0; surround < 2; ++surround) {
const int channel = surround + 3;
for (int group = 0; group < 24; group += 4) {
Complex current[4][64];
Complex dc_buffer[24];
for (int slot = 0; slot < 4; ++slot) {
for (int band = 0; band < 64; ++band) {
current[slot][band] = x_[channel][band][group + slot];
const Complex delayed = surround_delay_[surround][slot][band];
x_[channel][band][group + slot] = {delayed.im, -delayed.re};
}
}
for (int i = 0; i < 20; ++i) {
dc_buffer[i] = surround_history_[surround][i];
}
for (int i = 0; i < 4; ++i) {
dc_buffer[20 + i] = current[i][0];
}
for (int slot = 0; slot < 4; ++slot) {
Complex sum{0.0, 0.0};
for (int tap = 0; tap < 21; ++tap) {
const Complex sample = dc_buffer[slot + tap];
const double cr = kDcB[tap];
const double ci = kDcA[tap];
sum.re += sample.re * cr - sample.im * ci;
sum.im += sample.re * ci + sample.im * cr;
}
x_[channel][0][group + slot] = {2.0 * sum.re, 2.0 * sum.im};
}
for (int i = 0; i < 20; ++i) {
surround_history_[surround][i] = dc_buffer[i + 4];
}
std::memmove(&surround_delay_[surround][0][0],
&surround_delay_[surround][4][0],
sizeof(Complex) * 6 * 64);
for (int slot = 0; slot < 4; ++slot) {
std::memcpy(surround_delay_[surround][6 + slot], current[slot],
sizeof(Complex) * 64);
}
}
}
}
void analysis_channel(const int channel) noexcept {
const float* bed5 = job_bed5_;
const float phase_old = job_phase_old_;
const float phase_new = job_phase_new_;
const bool ramp_phase = phase_old != phase_new;
const float phase_step = static_cast<float>((phase_new - phase_old) / 256.0f);
float scaled[64];
float delayed[64];
for (int timeslot = 0; timeslot < 24; ++timeslot) {
for (int sample = 0; sample < 64; ++sample) {
const int frame_sample = timeslot * 64 + sample;
float gain = phase_new;
if (ramp_phase && frame_sample < 256) {
const float product = static_cast<float>(static_cast<float>(frame_sample) * phase_step);
gain = static_cast<float>(phase_old + product);
}
scaled[sample] = static_cast<float>(bed5[channel * 1536 + frame_sample] * gain);
}
const float* analysis_input = scaled;
if (channel < 3) {
std::memcpy(delayed, analysis_delay_[channel][0], sizeof(delayed));
std::memmove(&analysis_delay_[channel][0][0],
&analysis_delay_[channel][1][0],
sizeof(float) * 9 * 64);
std::memcpy(analysis_delay_[channel][9], scaled, sizeof(scaled));
analysis_input = delayed;
}
analysis_slot(channel, analysis_input, timeslot);
}
}
void analysis(const float* bed5, const float phase_new) noexcept {
job_bed5_ = bed5;
job_phase_old_ = analysis_phase_;
job_phase_new_ = phase_new;
job_kind_ = JobKind::Analysis;
job_count_ = 5;
for (int channel = 0; channel < 5; ++channel) {
job_objects_[channel] = channel;
}
dispatch_jobs();
analysis_phase_ = phase_new;
surround_post();
}
static std::size_t dq_index(const int object, const int point, const int channel, const int band) noexcept {
return static_cast<std::size_t>((((object * 2 + point) * 5 + channel) * kMaxBands) + band);
}
void matrix_object(
const int object,
const uint8_t* n_bands,
const uint8_t* n_dpoints,
const uint8_t* slope_idx,
const uint8_t* offset_ts,
const double* dq) noexcept {
std::memset(z_[object], 0, sizeof(z_[object]));
const int bands = n_bands[object];
const int points = n_dpoints[object];
const int slope = slope_idx[object];
const uint8_t* pb_map = parameter_band_map(bands);
for (int channel = 0; channel < 5; ++channel) {
for (int subband = 0; subband < 64; ++subband) {
const int parameter_band = pb_map[subband];
const double previous = matrix_previous_[object][channel][subband];
const double target0 = dq[dq_index(object, 0, channel, parameter_band)];
const double target1 = points == 2
? dq[dq_index(object, 1, channel, parameter_band)]
: target0;
double last = previous;
for (int timeslot = 0; timeslot < 24; ++timeslot) {
double coefficient;
if (slope == 0) {
if (points == 1) {
const double alpha = static_cast<double>(timeslot + 1) / 24.0;
coefficient = previous * (1.0 - alpha) + target0 * alpha;
} else if (timeslot < 12) {
const double alpha = static_cast<double>(timeslot + 1) / 12.0;
coefficient = previous * (1.0 - alpha) + target0 * alpha;
} else {
const double alpha = static_cast<double>(timeslot - 11) / 12.0;
coefficient = target0 * (1.0 - alpha) + target1 * alpha;
}
} else if (points == 1) {
coefficient = timeslot < offset_ts[object * 2] ? previous : target0;
} else {
coefficient = timeslot < offset_ts[object * 2] ? previous : target0;
if (timeslot >= offset_ts[object * 2 + 1]) {
coefficient = target1;
}
}
z_[object][subband][timeslot].re += x_[channel][subband][timeslot].re * coefficient;
z_[object][subband][timeslot].im += x_[channel][subband][timeslot].im * coefficient;
last = coefficient;
}
matrix_previous_[object][channel][subband] = last;
}
}
}
void qmf5_step(double* state, const double* rotated, double* output) noexcept {
for (int block = 0; block < 16; ++block) {
for (int lane = 0; lane < 4; ++lane) {
const int sample = block * 4 + lane;
const int state_base = block * 36 + lane;
const double even = rotated[block * 8 + lane * 2];
const double odd = rotated[block * 8 + lane * 2 + 1];
output[sample] = 2.0 * (
tables::kQmf5Window[sample] * even + state[state_base]);
state[state_base] = tables::kQmf5Window[1 * 64 + sample] * odd + state[state_base + 4];
for (int slot = 0; slot < 7; ++slot) {
const double alternating = (slot & 1) == 0 ? even : odd;
state[state_base + (slot + 1) * 4] =
tables::kQmf5Window[(slot + 2) * 64 + sample] * alternating +
state[state_base + (slot + 2) * 4];
}
state[state_base + 8 * 4] = tables::kQmf5Window[9 * 64 + sample] * odd;
}
}
}
void synthesis_object(
const int object,
const double clipgain,
const float output_scale,
float* output16) noexcept {
Complex frequency[64];
double rotated[128];
double pcm64[64];
double* state = synthesis_state_[object];
float* destination = output16 + (object + 1) * 1536;
for (int timeslot = 0; timeslot < 24; ++timeslot) {
for (int k = 0; k < 32; ++k) {
const Complex even = z_[object][2 * k][timeslot];
const Complex odd = z_[object][2 * k + 1][timeslot];
frequency[k] = {even.re, -even.im};
frequency[63 - k] = {odd.re, odd.im};
}
fft64(frequency);
for (int k = 0; k < 64; ++k) {
const double re = frequency[k].re;
const double im = frequency[k].im;
const double sin_component = rotation_sin_[k];
const double cos_component = rotation_cos_[k];
rotated[2 * k] = 2.0 * (re * cos_component + im * sin_component);
rotated[2 * k + 1] = 2.0 * (im * cos_component - re * sin_component);
}
qmf5_step(state, rotated, pcm64);
for (int sample = 0; sample < 64; ++sample) {
const double clipped = clamp_unit(16.0 * pcm64[sample]);
const float value = static_cast<float>(clipped * clipgain);
destination[timeslot * 64 + sample] = static_cast<float>(value * output_scale);
}
}
}
void process_one_object(const int object) noexcept {
matrix_object(object, job_n_bands_, job_n_dpoints_, job_slope_idx_,
job_offset_ts_, job_dq_);
synthesis_object(object, job_clipgain_, job_output_scale_, job_output16_);
}
void execute_job_loop() noexcept {
while (true) {
const int index = next_job_.fetch_add(1, std::memory_order_relaxed);
if (index >= job_count_) {
break;
}
const int item = job_objects_[index];
if (job_kind_ == JobKind::Analysis) {
analysis_channel(item);
} else {
process_one_object(item);
}
}
}
void dispatch_jobs() noexcept {
if (workers_.empty() || job_count_ == 1) {
next_job_.store(0, std::memory_order_relaxed);
execute_job_loop();
return;
}
next_job_.store(0, std::memory_order_relaxed);
work_barrier_->arrive_and_wait();
execute_job_loop();
work_barrier_->arrive_and_wait();
}
void worker_loop() noexcept {
while (!pool_ready_.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
if (stop_.load(std::memory_order_acquire)) {
return;
}
while (true) {
work_barrier_->arrive_and_wait();
if (stop_.load(std::memory_order_acquire)) {
return;
}
execute_job_loop();
work_barrier_->arrive_and_wait();
}
}
void stop_workers() noexcept {
if (workers_.empty()) {
work_barrier_.reset();
stop_.store(false, std::memory_order_relaxed);
pool_ready_.store(false, std::memory_order_relaxed);
return;
}
stop_.store(true, std::memory_order_release);
work_barrier_->arrive_and_wait();
for (std::thread& worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
workers_.clear();
work_barrier_.reset();
stop_.store(false, std::memory_order_relaxed);
pool_ready_.store(false, std::memory_order_relaxed);
}
void process_objects(
const uint32_t object_mask,
const uint8_t* n_bands,
const uint8_t* n_dpoints,
const uint8_t* slope_idx,
const uint8_t* offset_ts,
const double* dq,
const double clipgain,
const float output_scale,
float* output16) noexcept {
int count = 0;
for (int object = 0; object < EJOC_MAX_OBJECTS; ++object) {
if (object_mask & (1u << object)) {
job_objects_[count++] = object;
}
}
if (count == 0) {
return;
}
job_kind_ = JobKind::Objects;
job_count_ = count;
job_n_bands_ = n_bands;
job_n_dpoints_ = n_dpoints;
job_slope_idx_ = slope_idx;
job_offset_ts_ = offset_ts;
job_dq_ = dq;
job_clipgain_ = clipgain;
job_output_scale_ = output_scale;
job_output16_ = output16;
dispatch_jobs();
}
void render_lfe(const float* lfe, const float output_scale, float* output16) noexcept {
if (!lfe) {
return;
}
for (int sample = 0; sample < kLfeDelay; ++sample) {
const float value = static_cast<float>(clamp_unit(lfe_delay_[sample]));
output16[sample] = static_cast<float>(value * output_scale);
}
for (int sample = kLfeDelay; sample < 1536; ++sample) {
const float value = static_cast<float>(clamp_unit(static_cast<double>(lfe[sample - kLfeDelay])));
output16[sample] = static_cast<float>(value * output_scale);
}
for (int sample = 0; sample < kLfeDelay; ++sample) {
lfe_delay_[sample] = static_cast<double>(lfe[sample + (1536 - kLfeDelay)]);
}
}
enum class JobKind : uint8_t { Analysis, Objects };
std::vector<std::thread> workers_;
std::unique_ptr<std::barrier<>> work_barrier_;
std::atomic<bool> stop_{false};
std::atomic<bool> pool_ready_{false};
std::atomic<int> next_job_{0};
int job_objects_[EJOC_MAX_OBJECTS]{};
int job_count_ = 0;
JobKind job_kind_ = JobKind::Objects;
const float* job_bed5_ = nullptr;
float job_phase_old_ = 0.0625f;
float job_phase_new_ = 0.0625f;
const uint8_t* job_n_bands_ = nullptr;
const uint8_t* job_n_dpoints_ = nullptr;
const uint8_t* job_slope_idx_ = nullptr;
const uint8_t* job_offset_ts_ = nullptr;
const double* job_dq_ = nullptr;
double job_clipgain_ = 1.0;
float job_output_scale_ = 1.0f;
float* job_output16_ = nullptr;
alignas(64) double analysis_fifo_[5][9][64];
alignas(64) float analysis_delay_[3][10][64];
float analysis_phase_;
alignas(64) Complex surround_delay_[2][10][64];
alignas(64) Complex surround_history_[2][20];
alignas(64) double lfe_delay_[kLfeDelay];
alignas(64) double matrix_previous_[15][5][64];
alignas(64) double synthesis_state_[15][640];
alignas(64) Complex x_[5][64][24];
alignas(64) Complex z_[15][64][24];
uint8_t bit_reverse_[64];
Complex fft_twiddle_[32];
double rotation_sin_[64];
double rotation_cos_[64];
char error_[256];
};
} // namespace ejoc
extern "C" {
uint32_t EJOC_CALL ejoc_abi_version(void) {
return EJOC_ABI_VERSION;
}
const char* EJOC_CALL ejoc_build_info(void) {
#if defined(_MSC_VER)
return "eac3joc-core abi=1 compiler=MSVC fft=fixed64 speaker=double 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"
+71
View File
@@ -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
+18
View File
@@ -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
+388
View File
@@ -0,0 +1,388 @@
#pragma once
#include <stdint.h>
// Generated table data; do not hand-edit.
namespace ejoc::tables {
inline constexpr double kAnalysisWindow[640] = {
0.00019903187057934701, 0.00024947625934146345, 0.00030217695166356862, 0.00035484600812196732,
0.0004058915947098285, 0.00045464080176316202, 0.00050126801943406463, 0.00054649583762511611,
0.00059120741207152605, 0.0006361178238876164, 0.00068160606315359473, 0.00072772568091750145,
0.00077434181002900004, 0.00082129903603345156, 0.00086853635730221868, 0.00091610715026035905,
0.00096411682898178697, 0.0010126305278390646, 0.0010616052895784378, 0.0011108826147392392,
0.0011602368904277682, 0.0012094489065930247, 0.0012583627831190825, 0.0013069023843854666,
0.0013550462899729609, 0.0014027846045792103, 0.0014500867109745741, 0.0014968989416956902,
0.0015431707724928856, 0.0015888890484347939, 0.0016340982401743531, 0.0016788924112915993,
0.0017233812250196934, 0.0017676511779427528, 0.0018117419676855206, 0.0018556505674496293,
0.0018993609119206667, 0.0019428766099736094, 0.0019862416666001081, 0.0020295341964811087,
0.0020728406962007284, 0.0021162291523069143, 0.0021597379818558693, 0.0022033930290490389,
0.0022472396958619356, 0.0022913739085197449, 0.0023359460756182671, 0.0023811329156160355,
0.0024270866997539997, 0.0024738919455558062, 0.0025215502828359604, 0.0025700139813125134,
0.0026192441582679749, 0.0026692659594118595, 0.002720177173614502, 0.0027720888610929251,
0.0028250094037503004, 0.0028787164483219385, 0.0029326770454645157, 0.0029860674403607845,
0.0030379060190171003, 0.0030872693751007318, 0.0031335193198174238, 0.0031764607410877943,
0.0032163741998374462, 0.0032539025414735079, 0.0032898378558456898, 0.0033248732797801495,
0.003359407652169466, 0.0033934540115296841, 0.0034266682341694832, 0.0034584659151732922,
0.0034881711471825838, 0.0035151413176208735, 0.0035388274118304253, 0.0035587677266448736,
0.0035745392087846994, 0.0035856980830430984, 0.0035917432978749275, 0.0035921167582273483,
0.0035862282384186983, 0.0035734928678721189, 0.0035533567424863577, 0.0035253004170954227,
0.0034888240043073893, 0.003443423192948103, 0.0033885682933032513, 0.0033236993476748466,
0.0032482317183166742, 0.0031615688931196928, 0.0030631136614829302, 0.0029522709082812071,
0.0028284420259296894, 0.0026910160668194294, 0.0025393660180270672, 0.0023728485684841871,
0.0021908141206949949, 0.0019926181994378567, 0.0017776311142370105, 0.0015452421503141522,
0.001294855959713459, 0.0010258855763822794, 0.00073774566408246756, 0.00042984966421499848,
0.00010161137470277026, -0.00024754938203841448, -0.00061819725669920444, -0.0010108760325238109,
-0.001426108181476593, -0.0018643926596269011, -0.0023262077011168003, -0.002812013728544116,
-0.0033222525380551815, -0.003857344388961792, -0.0044176783412694931, -0.0050036045722663403,
-0.0056154225021600723, -0.0062533821910619736, -0.0069176913239061832, -0.0076085370965301991,
-0.008326113224029541, -0.0090706516057252884, -0.0098424339666962624, -0.010641784407198429,
-0.011469035409390926, -0.012324465438723564, -0.013208229094743729, -0.014120301231741905,
0.015060451813042164, 0.016028247773647308, 0.017023105174303055, 0.018044359982013702,
0.019091326743364334, 0.020163353532552719, 0.021259821951389313, 0.022380130365490913,
0.02352365106344223, 0.024689681828022003, 0.025877414271235466, 0.027085918933153152,
0.028314167633652687, 0.029561035335063934, 0.030825328081846237, 0.032105788588523865,
0.033401083201169968, 0.034709792584180832, 0.036030396819114685, 0.037361271679401398,
0.03870067372918129, 0.040046781301498413, 0.041397668421268463, 0.042751342058181763,
0.044105727225542068, 0.045458663254976273, 0.046807888895273209, 0.048151064664125443,
0.049485750496387482, 0.050809424370527267, 0.05211947113275528, 0.053413204848766327,
0.054687850177288055, 0.055940557271242142, 0.057168368250131607, 0.058368254452943802,
0.059537097811698914, 0.06067170575261116, 0.061768818646669388, 0.062825113534927368,
0.063837200403213501, 0.064801648259162903, 0.065714947879314423, 0.066573545336723328,
0.067373812198638916, 0.068112112581729889, 0.068784743547439575, 0.069387979805469513,
0.069918066263198853, 0.070371203124523163, 0.070743560791015625, 0.071031264960765839,
0.071230456233024597, 0.071337237954139709, 0.071347743272781372, 0.071258097887039185,
0.07106444239616394, 0.070762887597084045, 0.070349536836147308, 0.069820456206798553,
0.069171726703643799, 0.068399444222450256, 0.067499779164791107, 0.066468983888626099,
0.065303429961204529, 0.063999593257904053, 0.062554046511650085, 0.060963429510593414,
0.059224434196949005, 0.057333782315254211, 0.055288247764110565, 0.05308464914560318,
0.050719890743494034, 0.04819098487496376, 0.045495055615901947, 0.042629346251487732,
0.039591230452060699, 0.036378197371959686, 0.032987859100103378, 0.029417969286441803,
0.025666400790214539, 0.0217311792075634, 0.017610486596822739, 0.013302664272487164,
0.0088062174618244171, 0.0041198157705366611, -0.00075770384864881635, -0.0058273370377719402,
-0.011089906096458435, -0.016546055674552917, -0.022196246311068535, -0.028040755540132523,
-0.034079667180776596, -0.040312871336936951, -0.046740073710680008, -0.053360763937234879,
-0.06017424538731575, -0.0671796053647995, -0.074375726282596588, -0.081761270761489868,
-0.089334696531295776, -0.097094230353832245, -0.10503791272640228, -0.1131635457277298,
-0.12146873027086258, -0.12995083630084991, -0.13860704004764557, -0.1474342942237854,
-0.15642932057380676, -0.1655886322259903, -0.17490856349468231, -0.18438516557216644,
-0.19401434063911438, -0.20379173755645752, -0.21371282637119293, -0.22377283871173859,
-0.23396681249141693, -0.24428960680961609, -0.25473582744598389, -0.26529994606971741,
-0.27597621083259583, -0.2867586612701416, -0.29764124751091003, -0.30861768126487732,
-0.31968152523040771, -0.33082622289657593, -0.34204500913619995, -0.35333094000816345,
0.36467701196670532, 0.37607598304748535, 0.38752046227455139, 0.39900293946266174,
0.41051584482192993, 0.42205137014389038, 0.4336017370223999, 0.44515895843505859,
0.45671501755714417, 0.46826183795928955, 0.47979119420051575, 0.49129492044448853,
0.50276464223861694, 0.51419198513031006, 0.52556860446929932, 0.53688603639602661,
0.54813587665557861, 0.55930960178375244, 0.57039880752563477, 0.58139497041702271,
0.59228962659835815, 0.60307443141937256, 0.61374092102050781, 0.62428075075149536,
0.63468557596206665, 0.64494723081588745, 0.65505754947662354, 0.66500836610794067,
0.67479169368743896, 0.68439966440200806, 0.69382447004318237, 0.70305842161178589,
0.71209394931793213, 0.72092366218566895, 0.72954028844833374, 0.73793661594390869,
0.74610573053359985, 0.75404083728790283, 0.76173526048660278, 0.76918256282806396,
0.77637648582458496, 0.78331100940704346, 0.78998017311096191, 0.79637837409973145,
0.80250018835067749, 0.80834043025970459, 0.81389403343200684, 0.81915634870529175,
0.82412278652191162, 0.82878917455673218, 0.83315145969390869, 0.83720588684082031,
0.84094899892807007, 0.84437751770019531, 0.84748858213424683, 0.85027939081192017,
0.85274755954742432, 0.85489106178283691, 0.85670793056488037, 0.8581966757774353,
0.85935592651367188, 0.86018466949462891, 0.86068224906921387, 0.86084812879562378,
0.86068224906921387, 0.86018466949462891, 0.85935592651367188, 0.8581966757774353,
0.85670793056488037, 0.85489106178283691, 0.85274755954742432, 0.85027939081192017,
0.84748858213424683, 0.84437751770019531, 0.84094899892807007, 0.83720588684082031,
0.83315145969390869, 0.82878917455673218, 0.82412278652191162, 0.81915634870529175,
0.81389403343200684, 0.80834043025970459, 0.80250018835067749, 0.79637837409973145,
0.78998017311096191, 0.78331100940704346, 0.77637648582458496, 0.76918256282806396,
0.76173526048660278, 0.75404083728790283, 0.74610573053359985, 0.73793661594390869,
0.72954028844833374, 0.72092366218566895, 0.71209394931793213, 0.70305842161178589,
0.69382447004318237, 0.68439966440200806, 0.67479169368743896, 0.66500836610794067,
0.65505754947662354, 0.64494723081588745, 0.63468557596206665, 0.62428075075149536,
0.61374092102050781, 0.60307443141937256, 0.59228962659835815, 0.58139497041702271,
0.57039880752563477, 0.55930960178375244, 0.54813587665557861, 0.53688603639602661,
0.52556860446929932, 0.51419198513031006, 0.50276464223861694, 0.49129492044448853,
0.47979119420051575, 0.46826183795928955, 0.45671501755714417, 0.44515895843505859,
0.4336017370223999, 0.42205137014389038, 0.41051584482192993, 0.39900293946266174,
0.38752046227455139, 0.37607598304748535, 0.36467701196670532, 0.35333094000816345,
-0.34204500913619995, -0.33082622289657593, -0.31968152523040771, -0.30861768126487732,
-0.29764124751091003, -0.2867586612701416, -0.27597621083259583, -0.26529994606971741,
-0.25473582744598389, -0.24428960680961609, -0.23396681249141693, -0.22377283871173859,
-0.21371282637119293, -0.20379173755645752, -0.19401434063911438, -0.18438516557216644,
-0.17490856349468231, -0.1655886322259903, -0.15642932057380676, -0.1474342942237854,
-0.13860704004764557, -0.12995083630084991, -0.12146873027086258, -0.1131635457277298,
-0.10503791272640228, -0.097094230353832245, -0.089334696531295776, -0.081761270761489868,
-0.074375726282596588, -0.0671796053647995, -0.06017424538731575, -0.053360763937234879,
-0.046740073710680008, -0.040312871336936951, -0.034079667180776596, -0.028040755540132523,
-0.022196246311068535, -0.016546055674552917, -0.011089906096458435, -0.0058273370377719402,
-0.00075770384864881635, 0.0041198157705366611, 0.0088062174618244171, 0.013302664272487164,
0.017610486596822739, 0.0217311792075634, 0.025666400790214539, 0.029417969286441803,
0.032987859100103378, 0.036378197371959686, 0.039591230452060699, 0.042629346251487732,
0.045495055615901947, 0.04819098487496376, 0.050719890743494034, 0.05308464914560318,
0.055288247764110565, 0.057333782315254211, 0.059224434196949005, 0.060963429510593414,
0.062554046511650085, 0.063999593257904053, 0.065303429961204529, 0.066468983888626099,
0.067499779164791107, 0.068399444222450256, 0.069171726703643799, 0.069820456206798553,
0.070349536836147308, 0.070762887597084045, 0.07106444239616394, 0.071258097887039185,
0.071347743272781372, 0.071337237954139709, 0.071230456233024597, 0.071031264960765839,
0.070743560791015625, 0.070371203124523163, 0.069918066263198853, 0.069387979805469513,
0.068784743547439575, 0.068112112581729889, 0.067373812198638916, 0.066573545336723328,
0.065714947879314423, 0.064801648259162903, 0.063837200403213501, 0.062825113534927368,
0.061768818646669388, 0.06067170575261116, 0.059537097811698914, 0.058368254452943802,
0.057168368250131607, 0.055940557271242142, 0.054687850177288055, 0.053413204848766327,
0.05211947113275528, 0.050809424370527267, 0.049485750496387482, 0.048151064664125443,
0.046807888895273209, 0.045458663254976273, 0.044105727225542068, 0.042751342058181763,
0.041397668421268463, 0.040046781301498413, 0.03870067372918129, 0.037361271679401398,
0.036030396819114685, 0.034709792584180832, 0.033401083201169968, 0.032105788588523865,
0.030825328081846237, 0.029561035335063934, 0.028314167633652687, 0.027085918933153152,
0.025877414271235466, 0.024689681828022003, 0.02352365106344223, 0.022380130365490913,
0.021259821951389313, 0.020163353532552719, 0.019091326743364334, 0.018044359982013702,
0.017023105174303055, 0.016028247773647308, 0.015060451813042164, 0.014120301231741905,
-0.013208229094743729, -0.012324465438723564, -0.011469035409390926, -0.010641784407198429,
-0.0098424339666962624, -0.0090706516057252884, -0.008326113224029541, -0.0076085370965301991,
-0.0069176913239061832, -0.0062533821910619736, -0.0056154225021600723, -0.0050036045722663403,
-0.0044176783412694931, -0.003857344388961792, -0.0033222525380551815, -0.002812013728544116,
-0.0023262077011168003, -0.0018643926596269011, -0.001426108181476593, -0.0010108760325238109,
-0.00061819725669920444, -0.00024754938203841448, 0.00010161137470277026, 0.00042984966421499848,
0.00073774566408246756, 0.0010258855763822794, 0.001294855959713459, 0.0015452421503141522,
0.0017776311142370105, 0.0019926181994378567, 0.0021908141206949949, 0.0023728485684841871,
0.0025393660180270672, 0.0026910160668194294, 0.0028284420259296894, 0.0029522709082812071,
0.0030631136614829302, 0.0031615688931196928, 0.0032482317183166742, 0.0033236993476748466,
0.0033885682933032513, 0.003443423192948103, 0.0034888240043073893, 0.0035253004170954227,
0.0035533567424863577, 0.0035734928678721189, 0.0035862282384186983, 0.0035921167582273483,
0.0035917432978749275, 0.0035856980830430984, 0.0035745392087846994, 0.0035587677266448736,
0.0035388274118304253, 0.0035151413176208735, 0.0034881711471825838, 0.0034584659151732922,
0.0034266682341694832, 0.0033934540115296841, 0.003359407652169466, 0.0033248732797801495,
0.0032898378558456898, 0.0032539025414735079, 0.0032163741998374462, 0.0031764607410877943,
0.0031335193198174238, 0.0030872693751007318, 0.0030379060190171003, 0.0029860674403607845,
0.0029326770454645157, 0.0028787164483219385, 0.0028250094037503004, 0.0027720888610929251,
0.002720177173614502, 0.0026692659594118595, 0.0026192441582679749, 0.0025700139813125134,
0.0025215502828359604, 0.0024738919455558062, 0.0024270866997539997, 0.0023811329156160355,
0.0023359460756182671, 0.0022913739085197449, 0.0022472396958619356, 0.0022033930290490389,
0.0021597379818558693, 0.0021162291523069143, 0.0020728406962007284, 0.0020295341964811087,
0.0019862416666001081, 0.0019428766099736094, 0.0018993609119206667, 0.0018556505674496293,
0.0018117419676855206, 0.0017676511779427528, 0.0017233812250196934, 0.0016788924112915993,
0.0016340982401743531, 0.0015888890484347939, 0.0015431707724928856, 0.0014968989416956902,
0.0014500867109745741, 0.0014027846045792103, 0.0013550462899729609, 0.0013069023843854666,
0.0012583627831190825, 0.0012094489065930247, 0.0011602368904277682, 0.0011108826147392392,
0.0010616052895784378, 0.0010126305278390646, 0.00096411682898178697, 0.00091610715026035905,
0.00086853635730221868, 0.00082129903603345156, 0.00077434181002900004, 0.00072772568091750145,
0.00068160606315359473, 0.0006361178238876164, 0.00059120741207152605, 0.00054649583762511611,
0.00050126801943406463, 0.00045464080176316202, 0.0004058915947098285, 0.00035484600812196732,
0.00030217695166356862, 0.00024947625934146345, 0.00019903187057934701, 0,
};
inline constexpr double kQmf5Window[640] = {
0, 0.00019903187057934701, 0.00024947625934146345, 0.00030217695166356862,
0.00035484600812196732, 0.0004058915947098285, 0.00045464080176316202, 0.00050126801943406463,
0.00054649583762511611, 0.00059120741207152605, 0.0006361178238876164, 0.00068160606315359473,
0.00072772568091750145, 0.00077434181002900004, 0.00082129903603345156, 0.00086853635730221868,
0.00091610715026035905, 0.00096411682898178697, 0.0010126305278390646, 0.0010616052895784378,
0.0011108826147392392, 0.0011602368904277682, 0.0012094489065930247, 0.0012583627831190825,
0.0013069023843854666, 0.0013550462899729609, 0.0014027846045792103, 0.0014500867109745741,
0.0014968989416956902, 0.0015431707724928856, 0.0015888890484347939, 0.0016340982401743531,
0.0016788924112915993, 0.0017233812250196934, 0.0017676511779427528, 0.0018117419676855206,
0.0018556505674496293, 0.0018993609119206667, 0.0019428766099736094, 0.0019862416666001081,
0.0020295341964811087, 0.0020728406962007284, 0.0021162291523069143, 0.0021597379818558693,
0.0022033930290490389, 0.0022472396958619356, 0.0022913739085197449, 0.0023359460756182671,
0.0023811329156160355, 0.0024270866997539997, 0.0024738919455558062, 0.0025215502828359604,
0.0025700139813125134, 0.0026192441582679749, 0.0026692659594118595, 0.002720177173614502,
0.0027720888610929251, 0.0028250094037503004, 0.0028787164483219385, 0.0029326770454645157,
0.0029860674403607845, 0.0030379060190171003, 0.0030872693751007318, 0.0031335193198174238,
0.0031764607410877943, 0.0032163741998374462, 0.0032539025414735079, 0.0032898378558456898,
0.0033248732797801495, 0.003359407652169466, 0.0033934540115296841, 0.0034266682341694832,
0.0034584659151732922, 0.0034881711471825838, 0.0035151413176208735, 0.0035388274118304253,
0.0035587677266448736, 0.0035745392087846994, 0.0035856980830430984, 0.0035917432978749275,
0.0035921167582273483, 0.0035862282384186983, 0.0035734928678721189, 0.0035533567424863577,
0.0035253004170954227, 0.0034888240043073893, 0.003443423192948103, 0.0033885682933032513,
0.0033236993476748466, 0.0032482317183166742, 0.0031615688931196928, 0.0030631136614829302,
0.0029522709082812071, 0.0028284420259296894, 0.0026910160668194294, 0.0025393660180270672,
0.0023728485684841871, 0.0021908141206949949, 0.0019926181994378567, 0.0017776311142370105,
0.0015452421503141522, 0.001294855959713459, 0.0010258855763822794, 0.00073774566408246756,
0.00042984966421499848, 0.00010161137470277026, -0.00024754938203841448, -0.00061819725669920444,
-0.0010108760325238109, -0.001426108181476593, -0.0018643926596269011, -0.0023262077011168003,
-0.002812013728544116, -0.0033222525380551815, -0.003857344388961792, -0.0044176783412694931,
-0.0050036045722663403, -0.0056154225021600723, -0.0062533821910619736, -0.0069176913239061832,
-0.0076085370965301991, -0.008326113224029541, -0.0090706516057252884, -0.0098424339666962624,
-0.010641784407198429, -0.011469035409390926, -0.012324465438723564, -0.013208229094743729,
0.014120301231741905, 0.015060451813042164, 0.016028247773647308, 0.017023105174303055,
0.018044359982013702, 0.019091326743364334, 0.020163353532552719, 0.021259821951389313,
0.022380130365490913, 0.02352365106344223, 0.024689681828022003, 0.025877414271235466,
0.027085918933153152, 0.028314167633652687, 0.029561035335063934, 0.030825328081846237,
0.032105788588523865, 0.033401083201169968, 0.034709792584180832, 0.036030396819114685,
0.037361271679401398, 0.03870067372918129, 0.040046781301498413, 0.041397668421268463,
0.042751342058181763, 0.044105727225542068, 0.045458663254976273, 0.046807888895273209,
0.048151064664125443, 0.049485750496387482, 0.050809424370527267, 0.05211947113275528,
0.053413204848766327, 0.054687850177288055, 0.055940557271242142, 0.057168368250131607,
0.058368254452943802, 0.059537097811698914, 0.06067170575261116, 0.061768818646669388,
0.062825113534927368, 0.063837200403213501, 0.064801648259162903, 0.065714947879314423,
0.066573545336723328, 0.067373812198638916, 0.068112112581729889, 0.068784743547439575,
0.069387979805469513, 0.069918066263198853, 0.070371203124523163, 0.070743560791015625,
0.071031264960765839, 0.071230456233024597, 0.071337237954139709, 0.071347743272781372,
0.071258097887039185, 0.07106444239616394, 0.070762887597084045, 0.070349536836147308,
0.069820456206798553, 0.069171726703643799, 0.068399444222450256, 0.067499779164791107,
0.066468983888626099, 0.065303429961204529, 0.063999593257904053, 0.062554046511650085,
0.060963429510593414, 0.059224434196949005, 0.057333782315254211, 0.055288247764110565,
0.05308464914560318, 0.050719890743494034, 0.04819098487496376, 0.045495055615901947,
0.042629346251487732, 0.039591230452060699, 0.036378197371959686, 0.032987859100103378,
0.029417969286441803, 0.025666400790214539, 0.0217311792075634, 0.017610486596822739,
0.013302664272487164, 0.0088062174618244171, 0.0041198157705366611, -0.00075770384864881635,
-0.0058273370377719402, -0.011089906096458435, -0.016546055674552917, -0.022196246311068535,
-0.028040755540132523, -0.034079667180776596, -0.040312871336936951, -0.046740073710680008,
-0.053360763937234879, -0.06017424538731575, -0.0671796053647995, -0.074375726282596588,
-0.081761270761489868, -0.089334696531295776, -0.097094230353832245, -0.10503791272640228,
-0.1131635457277298, -0.12146873027086258, -0.12995083630084991, -0.13860704004764557,
-0.1474342942237854, -0.15642932057380676, -0.1655886322259903, -0.17490856349468231,
-0.18438516557216644, -0.19401434063911438, -0.20379173755645752, -0.21371282637119293,
-0.22377283871173859, -0.23396681249141693, -0.24428960680961609, -0.25473582744598389,
-0.26529994606971741, -0.27597621083259583, -0.2867586612701416, -0.29764124751091003,
-0.30861768126487732, -0.31968152523040771, -0.33082622289657593, -0.34204500913619995,
0.35333094000816345, 0.36467701196670532, 0.37607598304748535, 0.38752046227455139,
0.39900293946266174, 0.41051584482192993, 0.42205137014389038, 0.4336017370223999,
0.44515895843505859, 0.45671501755714417, 0.46826183795928955, 0.47979119420051575,
0.49129492044448853, 0.50276464223861694, 0.51419198513031006, 0.52556860446929932,
0.53688603639602661, 0.54813587665557861, 0.55930960178375244, 0.57039880752563477,
0.58139497041702271, 0.59228962659835815, 0.60307443141937256, 0.61374092102050781,
0.62428075075149536, 0.63468557596206665, 0.64494723081588745, 0.65505754947662354,
0.66500836610794067, 0.67479169368743896, 0.68439966440200806, 0.69382447004318237,
0.70305842161178589, 0.71209394931793213, 0.72092366218566895, 0.72954028844833374,
0.73793661594390869, 0.74610573053359985, 0.75404083728790283, 0.76173526048660278,
0.76918256282806396, 0.77637648582458496, 0.78331100940704346, 0.78998017311096191,
0.79637837409973145, 0.80250018835067749, 0.80834043025970459, 0.81389403343200684,
0.81915634870529175, 0.82412278652191162, 0.82878917455673218, 0.83315145969390869,
0.83720588684082031, 0.84094899892807007, 0.84437751770019531, 0.84748858213424683,
0.85027939081192017, 0.85274755954742432, 0.85489106178283691, 0.85670793056488037,
0.8581966757774353, 0.85935592651367188, 0.86018466949462891, 0.86068224906921387,
0.86084812879562378, 0.86068224906921387, 0.86018466949462891, 0.85935592651367188,
0.8581966757774353, 0.85670793056488037, 0.85489106178283691, 0.85274755954742432,
0.85027939081192017, 0.84748858213424683, 0.84437751770019531, 0.84094899892807007,
0.83720588684082031, 0.83315145969390869, 0.82878917455673218, 0.82412278652191162,
0.81915634870529175, 0.81389403343200684, 0.80834043025970459, 0.80250018835067749,
0.79637837409973145, 0.78998017311096191, 0.78331100940704346, 0.77637648582458496,
0.76918256282806396, 0.76173526048660278, 0.75404083728790283, 0.74610573053359985,
0.73793661594390869, 0.72954028844833374, 0.72092366218566895, 0.71209394931793213,
0.70305842161178589, 0.69382447004318237, 0.68439966440200806, 0.67479169368743896,
0.66500836610794067, 0.65505754947662354, 0.64494723081588745, 0.63468557596206665,
0.62428075075149536, 0.61374092102050781, 0.60307443141937256, 0.59228962659835815,
0.58139497041702271, 0.57039880752563477, 0.55930960178375244, 0.54813587665557861,
0.53688603639602661, 0.52556860446929932, 0.51419198513031006, 0.50276464223861694,
0.49129492044448853, 0.47979119420051575, 0.46826183795928955, 0.45671501755714417,
0.44515895843505859, 0.4336017370223999, 0.42205137014389038, 0.41051584482192993,
0.39900293946266174, 0.38752046227455139, 0.37607598304748535, 0.36467701196670532,
-0.35333094000816345, -0.34204500913619995, -0.33082622289657593, -0.31968152523040771,
-0.30861768126487732, -0.29764124751091003, -0.2867586612701416, -0.27597621083259583,
-0.26529994606971741, -0.25473582744598389, -0.24428960680961609, -0.23396681249141693,
-0.22377283871173859, -0.21371282637119293, -0.20379173755645752, -0.19401434063911438,
-0.18438516557216644, -0.17490856349468231, -0.1655886322259903, -0.15642932057380676,
-0.1474342942237854, -0.13860704004764557, -0.12995083630084991, -0.12146873027086258,
-0.1131635457277298, -0.10503791272640228, -0.097094230353832245, -0.089334696531295776,
-0.081761270761489868, -0.074375726282596588, -0.0671796053647995, -0.06017424538731575,
-0.053360763937234879, -0.046740073710680008, -0.040312871336936951, -0.034079667180776596,
-0.028040755540132523, -0.022196246311068535, -0.016546055674552917, -0.011089906096458435,
-0.0058273370377719402, -0.00075770384864881635, 0.0041198157705366611, 0.0088062174618244171,
0.013302664272487164, 0.017610486596822739, 0.0217311792075634, 0.025666400790214539,
0.029417969286441803, 0.032987859100103378, 0.036378197371959686, 0.039591230452060699,
0.042629346251487732, 0.045495055615901947, 0.04819098487496376, 0.050719890743494034,
0.05308464914560318, 0.055288247764110565, 0.057333782315254211, 0.059224434196949005,
0.060963429510593414, 0.062554046511650085, 0.063999593257904053, 0.065303429961204529,
0.066468983888626099, 0.067499779164791107, 0.068399444222450256, 0.069171726703643799,
0.069820456206798553, 0.070349536836147308, 0.070762887597084045, 0.07106444239616394,
0.071258097887039185, 0.071347743272781372, 0.071337237954139709, 0.071230456233024597,
0.071031264960765839, 0.070743560791015625, 0.070371203124523163, 0.069918066263198853,
0.069387979805469513, 0.068784743547439575, 0.068112112581729889, 0.067373812198638916,
0.066573545336723328, 0.065714947879314423, 0.064801648259162903, 0.063837200403213501,
0.062825113534927368, 0.061768818646669388, 0.06067170575261116, 0.059537097811698914,
0.058368254452943802, 0.057168368250131607, 0.055940557271242142, 0.054687850177288055,
0.053413204848766327, 0.05211947113275528, 0.050809424370527267, 0.049485750496387482,
0.048151064664125443, 0.046807888895273209, 0.045458663254976273, 0.044105727225542068,
0.042751342058181763, 0.041397668421268463, 0.040046781301498413, 0.03870067372918129,
0.037361271679401398, 0.036030396819114685, 0.034709792584180832, 0.033401083201169968,
0.032105788588523865, 0.030825328081846237, 0.029561035335063934, 0.028314167633652687,
0.027085918933153152, 0.025877414271235466, 0.024689681828022003, 0.02352365106344223,
0.022380130365490913, 0.021259821951389313, 0.020163353532552719, 0.019091326743364334,
0.018044359982013702, 0.017023105174303055, 0.016028247773647308, 0.015060451813042164,
-0.014120301231741905, -0.013208229094743729, -0.012324465438723564, -0.011469035409390926,
-0.010641784407198429, -0.0098424339666962624, -0.0090706516057252884, -0.008326113224029541,
-0.0076085370965301991, -0.0069176913239061832, -0.0062533821910619736, -0.0056154225021600723,
-0.0050036045722663403, -0.0044176783412694931, -0.003857344388961792, -0.0033222525380551815,
-0.002812013728544116, -0.0023262077011168003, -0.0018643926596269011, -0.001426108181476593,
-0.0010108760325238109, -0.00061819725669920444, -0.00024754938203841448, 0.00010161137470277026,
0.00042984966421499848, 0.00073774566408246756, 0.0010258855763822794, 0.001294855959713459,
0.0015452421503141522, 0.0017776311142370105, 0.0019926181994378567, 0.0021908141206949949,
0.0023728485684841871, 0.0025393660180270672, 0.0026910160668194294, 0.0028284420259296894,
0.0029522709082812071, 0.0030631136614829302, 0.0031615688931196928, 0.0032482317183166742,
0.0033236993476748466, 0.0033885682933032513, 0.003443423192948103, 0.0034888240043073893,
0.0035253004170954227, 0.0035533567424863577, 0.0035734928678721189, 0.0035862282384186983,
0.0035921167582273483, 0.0035917432978749275, 0.0035856980830430984, 0.0035745392087846994,
0.0035587677266448736, 0.0035388274118304253, 0.0035151413176208735, 0.0034881711471825838,
0.0034584659151732922, 0.0034266682341694832, 0.0033934540115296841, 0.003359407652169466,
0.0033248732797801495, 0.0032898378558456898, 0.0032539025414735079, 0.0032163741998374462,
0.0031764607410877943, 0.0031335193198174238, 0.0030872693751007318, 0.0030379060190171003,
0.0029860674403607845, 0.0029326770454645157, 0.0028787164483219385, 0.0028250094037503004,
0.0027720888610929251, 0.002720177173614502, 0.0026692659594118595, 0.0026192441582679749,
0.0025700139813125134, 0.0025215502828359604, 0.0024738919455558062, 0.0024270866997539997,
0.0023811329156160355, 0.0023359460756182671, 0.0022913739085197449, 0.0022472396958619356,
0.0022033930290490389, 0.0021597379818558693, 0.0021162291523069143, 0.0020728406962007284,
0.0020295341964811087, 0.0019862416666001081, 0.0019428766099736094, 0.0018993609119206667,
0.0018556505674496293, 0.0018117419676855206, 0.0017676511779427528, 0.0017233812250196934,
0.0016788924112915993, 0.0016340982401743531, 0.0015888890484347939, 0.0015431707724928856,
0.0014968989416956902, 0.0014500867109745741, 0.0014027846045792103, 0.0013550462899729609,
0.0013069023843854666, 0.0012583627831190825, 0.0012094489065930247, 0.0011602368904277682,
0.0011108826147392392, 0.0010616052895784378, 0.0010126305278390646, 0.00096411682898178697,
0.00091610715026035905, 0.00086853635730221868, 0.00082129903603345156, 0.00077434181002900004,
0.00072772568091750145, 0.00068160606315359473, 0.0006361178238876164, 0.00059120741207152605,
0.00054649583762511611, 0.00050126801943406463, 0.00045464080176316202, 0.0004058915947098285,
0.00035484600812196732, 0.00030217695166356862, 0.00024947625934146345, 0.00019903187057934701,
};
inline constexpr uint8_t kPbMap1[64] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
inline constexpr uint8_t kPbMap3[64] = {
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
};
inline constexpr uint8_t kPbMap5[64] = {
0, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
};
inline constexpr uint8_t kPbMap7[64] = {
0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5,
5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
};
inline constexpr uint8_t kPbMap9[64] = {
0, 1, 2, 3, 3, 3, 4, 5, 5, 6, 6, 6, 7, 7, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
};
inline constexpr uint8_t kPbMap12[64] = {
0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8,
8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
};
inline constexpr uint8_t kPbMap15[64] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 10, 10, 11, 11, 11,
12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
};
inline constexpr uint8_t kPbMap23[64] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 12, 13, 13,
14, 14, 15, 15, 16, 16, 16, 17, 17, 17, 18, 18, 18, 18, 19, 19,
19, 19, 19, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
};
} // namespace ejoc::tables
+500
View File
@@ -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
+103
View File
@@ -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
+101
View File
@@ -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
+30
View File
@@ -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
+293
View File
@@ -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
+321
View File
@@ -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
+272
View File
@@ -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
+311
View File
@@ -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
+156
View File
@@ -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
+209
View File
@@ -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
+534
View File
@@ -0,0 +1,534 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
namespace ejoc::speaker_tables {
inline constexpr std::size_t kMaxPoints = 15;
inline constexpr std::size_t kMaxGroups = 4;
inline constexpr std::size_t kMaxGroupSize = 3;
inline constexpr std::size_t kMaxChannels = 16;
struct SpeakerPoint {
std::array<std::uint16_t, 3> coordinate_q15{};
std::uint8_t speaker_id{};
};
struct AxisGroup {
std::uint8_t size{};
std::array<std::uint8_t, kMaxGroupSize> indices{};
};
struct RegionGeometry {
std::uint8_t point_count{};
std::uint8_t mode{};
std::uint8_t axis0_group_count{};
std::uint8_t axis1_group_count{};
std::array<SpeakerPoint, kMaxPoints> points{};
std::array<AxisGroup, kMaxGroups> axis0_groups{};
std::array<AxisGroup, kMaxGroups> axis1_groups{};
};
struct LayoutGeometry {
std::uint32_t speaker_bitfield{};
std::uint8_t out_ch_config{};
std::uint8_t channel_count{};
std::array<std::uint8_t, kMaxChannels> standard_from_internal{};
std::array<RegionGeometry, 7> regions{};
};
inline constexpr std::array<LayoutGeometry, 10> kLayouts{{
LayoutGeometry{
0x1u, 0, 2,
{{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0x7u, 3, 4,
{{0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
1, 1, 1, 0,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0xFu, 7, 6,
{{0, 1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
5, 2, 2, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 2, 2, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 2, 2, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 2, 2, 0,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0x1Fu, 11, 8,
{{0, 1, 2, 3, 6, 7, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
7, 2, 3, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 2, 2, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 2, 2, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 2, 2, 0,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
4, 2, 2, 0,
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0x40Fu, 13, 8,
{{0, 1, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 2, 1,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 1, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
4, 3, 1, 1,
{{SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0xA0Fu, 14, 10,
{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 2,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 1, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
6, 3, 1, 2,
{{SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {2, 3, 0}}, AxisGroup{2, {4, 5, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 1, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0x41Fu, 15, 10,
{{0, 1, 2, 3, 6, 7, 4, 5, 8, 9, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
9, 3, 3, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}}},
{{AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 2, 1,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 1, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
6, 3, 2, 1,
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {4, 5, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0xA1Fu, 16, 12,
{{0, 1, 2, 3, 6, 7, 4, 5, 8, 9, 10, 11, 0, 0, 0, 0}},
{{
RegionGeometry{
11, 3, 3, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}}},
{{AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {9, 10, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 2,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 1, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
8, 3, 2, 2,
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {4, 5, 0}}, AxisGroup{2, {6, 7, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 1, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0xA9Fu, 19, 14,
{{0, 1, 2, 3, 6, 7, 4, 5, 10, 11, 12, 13, 8, 9, 0, 0}},
{{
RegionGeometry{
13, 3, 4, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}}},
{{AxisGroup{2, {9, 10, 0}}, AxisGroup{2, {11, 12, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
11, 3, 3, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}}},
{{AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {9, 10, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 2,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 1, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
8, 3, 2, 2,
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {4, 5, 0}}, AxisGroup{2, {6, 7, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0xE9Fu, 20, 16,
{{0, 1, 2, 3, 6, 7, 4, 5, 10, 11, 14, 15, 12, 13, 8, 9}},
{{
RegionGeometry{
15, 3, 4, 3,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}}},
{{AxisGroup{2, {9, 10, 0}}, AxisGroup{2, {11, 12, 0}}, AxisGroup{2, {13, 14, 0}}, AxisGroup{}}}
},
RegionGeometry{
13, 3, 3, 3,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}}},
{{AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {9, 10, 0}}, AxisGroup{2, {11, 12, 0}}, AxisGroup{}}}
},
RegionGeometry{
11, 3, 2, 3,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {9, 10, 0}}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 3,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 1, 3,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}}}
},
RegionGeometry{
10, 3, 2, 3,
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {4, 5, 0}}, AxisGroup{2, {6, 7, 0}}, AxisGroup{2, {8, 9, 0}}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
}
}};
inline constexpr const LayoutGeometry* find_layout(const std::uint32_t speaker_bitfield) noexcept {
for (const auto& layout : kLayouts) {
if (layout.speaker_bitfield == speaker_bitfield) {
return &layout;
}
}
return nullptr;
}
} // namespace ejoc::speaker_tables
+495
View File
@@ -0,0 +1,495 @@
#define EJOC_BUILD_DLL
#include "eac3joc_core.h"
#include "speaker_layouts.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <new>
namespace ejoc::speaker {
using speaker_tables::AxisGroup;
using speaker_tables::LayoutGeometry;
using speaker_tables::RegionGeometry;
constexpr double kPi = 3.141592653589793238462643383279502884;
constexpr double kQ15Scale = 32768.0;
constexpr double kQ15Max = 32767.0 / kQ15Scale;
constexpr double kGainSnapThreshold = 1.0e-4;
constexpr std::size_t kObjects = EJOC_MAX_OBJECTS;
constexpr std::size_t kChannels = EJOC_OUTPUT_CHANNELS;
constexpr std::size_t kBlock = EJOC_SPEAKER_BLOCK_SAMPLES;
using PointGains = std::array<double, speaker_tables::kMaxPoints>;
using ChannelGains = std::array<double, kChannels>;
using ObjectChannelGains = std::array<ChannelGains, kObjects>;
using RemainingCounts = std::array<std::array<std::uint32_t, kChannels>, kObjects>;
inline double clamp(const double value, const double low, const double high) noexcept {
return value < low ? low : (value > high ? high : value);
}
inline double coordinate(const RegionGeometry& region, const std::size_t point,
const std::size_t component) noexcept {
return static_cast<double>(region.points[point].coordinate_q15[component]) / kQ15Scale;
}
std::uint64_t expand_speaker_bitfield(const std::uint32_t compact) noexcept {
constexpr std::array<std::uint64_t, 22> expansions{{
0x00000003ULL, 0x00000004ULL, 0x00000008ULL, 0x00000030ULL,
0x000000C0ULL, 0x00000100ULL, 0x00000600ULL, 0x00001800ULL,
0x00006000ULL, 0x00018000ULL, 0x00060000ULL, 0x00180000ULL,
0x00600000ULL, 0x01800000ULL, 0x06000000ULL, 0x18000000ULL,
0x60000000ULL, 0x080000000ULL, 0x600000000ULL, 0x800000000ULL,
0x1000000000ULL, 0x2000000000ULL,
}};
std::uint64_t expanded = 0;
for (std::size_t bit_index = 0; bit_index < expansions.size(); ++bit_index) {
if ((compact & (1u << bit_index)) != 0) {
expanded |= expansions[bit_index];
}
}
return expanded;
}
inline int bit(const std::uint64_t value, const unsigned index) noexcept {
return static_cast<int>((value >> index) & 1ULL);
}
double layout_attenuation_db(const std::uint32_t compact) noexcept {
const std::uint64_t expanded = expand_speaker_bitfield(compact);
const int height_channels = 2 * (
bit(expanded, 13) + bit(expanded, 15) + bit(expanded, 17) +
bit(expanded, 19) + bit(expanded, 21));
const int floor_channels = bit(expanded, 8) + 2 * (
bit(expanded, 31) + bit(expanded, 4) + bit(expanded, 6) +
bit(expanded, 11) + bit(expanded, 25) + bit(expanded, 27) +
bit(expanded, 29) + bit(expanded, 33));
const double height_factor = std::min(static_cast<double>(height_channels) / 4.0, 1.0);
const double floor_factor = std::min(static_cast<double>(floor_channels) / 4.0, 1.0);
return -std::max(4.5 - 1.5 * height_factor - 3.0 * floor_factor, 0.0);
}
int floor_y_exponent(const std::uint32_t compact) noexcept {
const std::uint32_t low = static_cast<std::uint32_t>(expand_speaker_bitfield(compact));
return ((low & 0x130u) != 0 && (low & 0x18C0u) == 0) ? 1 : 0;
}
inline void equal_power_pair(const double position, double& lower, double& upper) noexcept {
const double angle = (kPi * 0.5) * position;
lower = std::cos(angle);
upper = std::sin(angle);
}
void axis0_gains(const RegionGeometry& region,
const std::array<AxisGroup, speaker_tables::kMaxGroups>& groups,
const std::uint8_t group_count,
const double value,
PointGains& output) noexcept {
output.fill(0.0);
for (std::size_t row = 0; row < group_count; ++row) {
const auto& group = groups[row];
if (group.size == 0) {
continue;
}
const std::size_t first = group.indices[0];
const std::size_t last = group.indices[group.size - 1];
const double first_value = coordinate(region, first, 0);
const double last_value = coordinate(region, last, 0);
if (value <= first_value) {
output[first] = 1.0;
continue;
}
if (value >= last_value) {
output[last] = 1.0;
continue;
}
for (std::size_t index = 0; index + 1 < group.size; ++index) {
const std::size_t lower_index = group.indices[index];
const std::size_t upper_index = group.indices[index + 1];
const double lower_value = coordinate(region, lower_index, 0);
const double upper_value = coordinate(region, upper_index, 0);
if (value > lower_value && value <= upper_value) {
const double position = (value - lower_value) / (upper_value - lower_value);
equal_power_pair(position, output[lower_index], output[upper_index]);
break;
}
}
}
}
void axis1_gains(const RegionGeometry& region,
const std::array<AxisGroup, speaker_tables::kMaxGroups>& groups,
const std::uint8_t group_count,
const double value,
PointGains& output) noexcept {
output.fill(0.0);
if (group_count == 0) {
return;
}
const auto& first_group = groups[0];
const auto& last_group = groups[group_count - 1];
const double first_value = coordinate(region, first_group.indices[0], 1);
const double last_value = coordinate(region, last_group.indices[0], 1);
if (value <= first_value) {
for (std::size_t index = 0; index < first_group.size; ++index) {
output[first_group.indices[index]] = 1.0;
}
return;
}
if (value > last_value) {
for (std::size_t index = 0; index < last_group.size; ++index) {
output[last_group.indices[index]] = 1.0;
}
return;
}
for (std::size_t row = 0; row + 1 < group_count; ++row) {
const auto& lower_group = groups[row];
const auto& upper_group = groups[row + 1];
const double lower_value = coordinate(region, lower_group.indices[0], 1);
const double upper_value = coordinate(region, upper_group.indices[0], 1);
if (value >= lower_value && value <= upper_value) {
const double position = (value - lower_value) / (upper_value - lower_value);
double lower_gain = 0.0;
double upper_gain = 0.0;
equal_power_pair(position, lower_gain, upper_gain);
for (std::size_t index = 0; index < lower_group.size; ++index) {
output[lower_group.indices[index]] = lower_gain;
}
for (std::size_t index = 0; index < upper_group.size; ++index) {
output[upper_group.indices[index]] = upper_gain;
}
return;
}
}
}
void plane_gains(const RegionGeometry& region,
const std::array<AxisGroup, speaker_tables::kMaxGroups>& groups,
const std::uint8_t group_count,
const double u,
const double v,
const std::uint8_t mode,
PointGains& output) noexcept {
axis0_gains(region, groups, group_count, u, output);
if (mode >= 2) {
PointGains vertical{};
axis1_gains(region, groups, group_count, v, vertical);
for (std::size_t point = 0; point < region.point_count; ++point) {
output[point] *= vertical[point];
}
}
}
class Renderer {
public:
explicit Renderer(const LayoutGeometry* layout) noexcept : layout_(layout) {
for (std::size_t standard = 0; standard < layout_->channel_count; ++standard) {
standard_index_for_internal_[layout_->standard_from_internal[standard]] =
static_cast<std::uint8_t>(standard);
}
attenuation_db_ = layout_attenuation_db(layout_->speaker_bitfield);
floor_y_exponent_ = floor_y_exponent(layout_->speaker_bitfield);
reset_state();
}
int reset() noexcept {
reset_state();
error_[0] = '\0';
return 0;
}
const char* last_error() const noexcept {
return error_[0] ? error_.data() : "";
}
int process(const float* input,
const std::uint32_t sample_count,
const std::uint32_t metadata_count,
const std::uint32_t* metadata_offsets,
const std::uint32_t* ramp_durations,
const std::uint16_t* positions_q15,
const std::uint8_t* region_indices,
const std::uint8_t* height_enabled,
const double* object_gains,
double* output) noexcept {
error_[0] = '\0';
if (!input || !output) {
return fail("null PCM pointer passed to ejoc_speaker_renderer_process");
}
if ((sample_count % kBlock) != 0) {
return fail("sample_count must be a multiple of 32");
}
if (metadata_count && (!metadata_offsets || !ramp_durations || !positions_q15)) {
return fail("metadata arrays are null while metadata_count is nonzero");
}
if (!validate_metadata(sample_count, metadata_count, metadata_offsets,
positions_q15, region_indices, object_gains)) {
return -1;
}
std::fill(output, output + static_cast<std::size_t>(sample_count) * layout_->channel_count, 0.0);
const bool has_lfe = (layout_->speaker_bitfield & 0x4u) != 0;
const std::size_t total_blocks = sample_count / kBlock;
std::size_t event = 0;
for (std::size_t block = 0; block < total_blocks; ++block) {
while (event < metadata_count && aligned_block(metadata_offsets[event]) == block) {
apply_event(event, ramp_durations, positions_q15, region_indices,
height_enabled, object_gains);
++event;
}
mix_block(input, output, block, has_lfe);
}
while (event < metadata_count && aligned_block(metadata_offsets[event]) == total_blocks) {
apply_event(event, ramp_durations, positions_q15, region_indices,
height_enabled, object_gains);
++event;
}
if (event != metadata_count) {
return fail("metadata alignment produced an event outside this process call");
}
return 0;
}
private:
void reset_state() noexcept {
for (auto& row : current_) row.fill(0.0);
for (auto& row : target_) row.fill(0.0);
for (auto& row : step_) row.fill(0.0);
for (auto& row : remaining_) row.fill(0);
}
int fail(const char* message) noexcept {
std::snprintf(error_.data(), error_.size(), "%s", message);
return -1;
}
bool validate_metadata(const std::uint32_t sample_count,
const std::uint32_t metadata_count,
const std::uint32_t* metadata_offsets,
const std::uint16_t* positions_q15,
const std::uint8_t* region_indices,
const double* object_gains) noexcept {
for (std::size_t event = 0; event < metadata_count; ++event) {
if (metadata_offsets[event] > sample_count) {
fail("metadata offset exceeds sample_count");
return false;
}
if (event && metadata_offsets[event] < metadata_offsets[event - 1]) {
fail("metadata offsets must be nondecreasing");
return false;
}
for (std::size_t object = 0; object < kObjects; ++object) {
const std::size_t object_event = event * kObjects + object;
if (region_indices && region_indices[object_event] >= 7) {
fail("region index is above 6");
return false;
}
if (object_gains && !std::isfinite(object_gains[object_event])) {
fail("object gain is not finite");
return false;
}
const std::size_t coordinate_base = object_event * EJOC_SPEAKER_COORDINATES;
for (std::size_t component = 0; component < EJOC_SPEAKER_COORDINATES; ++component) {
if (positions_q15[coordinate_base + component] > 32767u) {
fail("Q15 object coordinate is above 32767");
return false;
}
}
}
}
return true;
}
static std::size_t aligned_block(const std::uint32_t sample) noexcept {
return (static_cast<std::size_t>(sample) + kBlock / 2 - 1) / kBlock;
}
static std::uint32_t ramp_blocks(const std::uint32_t duration) noexcept {
return static_cast<std::uint32_t>(
(static_cast<std::size_t>(duration) + kBlock / 2 - 1) / kBlock);
}
void render_point(const std::uint16_t* position,
const std::uint8_t region_index,
const bool enable_height,
const double object_gain,
ChannelGains& output) const noexcept {
output.fill(0.0);
const auto& region = layout_->regions[region_index];
const double u = static_cast<double>(position[0]) / kQ15Scale;
const double v = static_cast<double>(position[1]) / kQ15Scale;
const double w = static_cast<double>(position[2]) / kQ15Scale;
const double floor_v = clamp(std::ldexp(v, floor_y_exponent_), 0.0, 1.0);
PointGains floor{};
plane_gains(region, region.axis0_groups, region.axis0_group_count,
u, floor_v, region.mode, floor);
PointGains point = floor;
if (region.mode == 3) {
PointGains height{};
plane_gains(region, region.axis1_groups, region.axis1_group_count,
u, v, 3, height);
const double z = enable_height ? clamp(w, 0.0, kQ15Max) : 0.0;
if (z >= kQ15Max) {
point = height;
} else if (z > 0.0) {
double floor_weight = 0.0;
double height_weight = 0.0;
equal_power_pair(z, floor_weight, height_weight);
for (std::size_t index = 0; index < region.point_count; ++index) {
point[index] = floor[index] * floor_weight + height[index] * height_weight;
}
}
}
const double y_term = clamp(v / 0.6, 0.0, 1.0);
const double z_term = clamp((w - 0.2) / 0.8, 0.0, 1.0);
const double amount = clamp(y_term + z_term, 0.0, 1.0);
const double gain = std::pow(10.0, attenuation_db_ * amount / 20.0) * object_gain;
for (std::size_t index = 0; index < region.point_count; ++index) {
output[region.points[index].speaker_id] = point[index] * gain;
}
}
void apply_event(const std::size_t event,
const std::uint32_t* ramp_durations,
const std::uint16_t* positions_q15,
const std::uint8_t* region_indices,
const std::uint8_t* height_enabled,
const double* object_gains) noexcept {
const std::uint32_t blocks = ramp_blocks(ramp_durations[event]);
for (std::size_t object = 0; object < kObjects; ++object) {
const std::size_t object_event = event * kObjects + object;
const auto* position = positions_q15 + object_event * EJOC_SPEAKER_COORDINATES;
const std::uint8_t region = region_indices ? region_indices[object_event] : 0;
const bool height = !height_enabled || height_enabled[object_event] != 0;
const double object_gain = object_gains ? object_gains[object_event] : 1.0;
ChannelGains next{};
render_point(position, region, height, object_gain, next);
for (std::size_t channel = 0; channel < layout_->channel_count; ++channel) {
const double difference = next[channel] - current_[object][channel];
target_[object][channel] = next[channel];
if (std::abs(difference) >= kGainSnapThreshold && blocks != 0) {
step_[object][channel] = difference / static_cast<double>(blocks);
remaining_[object][channel] = blocks;
} else {
current_[object][channel] = next[channel];
step_[object][channel] = 0.0;
remaining_[object][channel] = 0;
}
}
}
}
void mix_block(const float* input, double* output, const std::size_t block,
const bool has_lfe) noexcept {
const std::size_t start = block * kBlock;
if (has_lfe) {
for (std::size_t sample = 0; sample < kBlock; ++sample) {
output[(start + sample) * layout_->channel_count + 3] =
static_cast<double>(input[(start + sample) * EJOC_OUTPUT_CHANNELS]);
}
}
for (std::size_t object = 0; object < kObjects; ++object) {
for (std::size_t internal = 0; internal < layout_->channel_count; ++internal) {
const bool active = remaining_[object][internal] != 0;
const double fixed_gain = target_[object][internal];
if (!active && fixed_gain == 0.0) {
continue;
}
const std::size_t standard = standard_index_for_internal_[internal];
for (std::size_t sample = 0; sample < kBlock; ++sample) {
const double gain = active
? current_[object][internal] +
(static_cast<double>(sample) / static_cast<double>(kBlock)) *
step_[object][internal]
: fixed_gain;
output[(start + sample) * layout_->channel_count + standard] +=
static_cast<double>(
input[(start + sample) * EJOC_OUTPUT_CHANNELS + object + 1]) * gain;
}
if (active) {
current_[object][internal] += step_[object][internal];
--remaining_[object][internal];
if (remaining_[object][internal] == 0) {
current_[object][internal] = target_[object][internal];
}
} else {
current_[object][internal] = target_[object][internal];
}
}
}
}
const LayoutGeometry* layout_;
double attenuation_db_{};
int floor_y_exponent_{};
ObjectChannelGains current_{};
ObjectChannelGains target_{};
ObjectChannelGains step_{};
RemainingCounts remaining_{};
std::array<std::uint8_t, kChannels> standard_index_for_internal_{};
std::array<char, 256> error_{};
};
} // namespace ejoc::speaker
extern "C" {
uint32_t EJOC_CALL ejoc_speaker_layout_channel_count(const uint32_t speaker_bitfield) {
const auto* layout = ejoc::speaker_tables::find_layout(speaker_bitfield);
return layout ? layout->channel_count : 0;
}
ejoc_speaker_renderer_handle EJOC_CALL ejoc_speaker_renderer_create(
const uint32_t speaker_bitfield) {
const auto* layout = ejoc::speaker_tables::find_layout(speaker_bitfield);
if (!layout) {
return nullptr;
}
return new (std::nothrow) ejoc::speaker::Renderer(layout);
}
void EJOC_CALL ejoc_speaker_renderer_destroy(ejoc_speaker_renderer_handle handle) {
delete static_cast<ejoc::speaker::Renderer*>(handle);
}
int EJOC_CALL ejoc_speaker_renderer_reset(ejoc_speaker_renderer_handle handle) {
if (!handle) {
return -1;
}
return static_cast<ejoc::speaker::Renderer*>(handle)->reset();
}
const char* EJOC_CALL ejoc_speaker_renderer_last_error(ejoc_speaker_renderer_handle handle) {
if (!handle) {
return "speaker renderer handle is null";
}
return static_cast<ejoc::speaker::Renderer*>(handle)->last_error();
}
int EJOC_CALL ejoc_speaker_renderer_process(
ejoc_speaker_renderer_handle handle,
const float* objects16_interleaved,
const uint32_t sample_count,
const uint32_t metadata_count,
const uint32_t* metadata_offsets,
const uint32_t* ramp_durations,
const uint16_t* positions_q15,
const uint8_t* region_indices,
const uint8_t* height_enabled,
const double* object_gains,
double* output_interleaved) {
if (!handle) {
return -1;
}
return static_cast<ejoc::speaker::Renderer*>(handle)->process(
objects16_interleaved, sample_count, metadata_count,
metadata_offsets, ramp_durations, positions_q15,
region_indices, height_enabled, object_gains, output_interleaved);
}
} // extern "C"
+62
View File
@@ -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
+31
View File
@@ -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
+474
View File
@@ -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
+125
View File
@@ -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
+22
View File
@@ -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
+111
View File
@@ -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
+62
View File
@@ -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