Last Updated: May 12, 2026
Source: source/spatial_engine/spatialRender/SpatialRenderer.cpp, source/spatial_engine/spatialRender/OfflineOutputRouteMap.cpp, source/spatial_engine/src/JSONLoader.cpp
See PUBLIC_DOCS/API.md for the public input-contract summary and DEPENDENCIES.md for the maintainer-level LUSID and layout format spec.
RENDERING.md— Comprehensive reference for the offline spatial renderer (spatialroot_spatial_render).
Three spatializers supported:
| Feature | DBAP (default) | LBAP |
|---|---|---|
| Coverage | No gaps (works anywhere) | No gaps |
| Layout Req | Any layout | Multi-ring layers |
| Localization | Moderate | Moderate |
| Speakers/Src | Distance-weighted (many) | Layer interpolation |
| Best For | Unknown/irregular layouts | AlloSphere, TransLAB |
| Params | --dbap_focus (0.1–5.0) |
--lbap_dispersion (0–1.0) |
Pipeline: Source WAVs + LUSID scene + Layout JSON -> compact internal bus -> layout-derived device-indexed N-channel WAV
Offline output routing now preserves layout channel assignments. The renderer spatializes into a compact internal bus (main speakers + subwoofers), then scatters those internal channels into the final WAV using OfflineOutputRouteMap. Final WAV width is max(channel) + 1; unmapped channels are included and silent. See REALTIME_ENGINE.md § Output Routing Architecture for the shared two-space routing model.
Current scope note: the offline CLI is maintained, but the GUI's Offline Render controls remain intentionally hidden until parity and workflow validation are complete. Treat source/spatial_engine/spatialRender/ as the offline-owned implementation surface.
# Default render with DBAP
./build/source/spatial_engine/spatialRender/spatialroot_spatial_render \
--layout source/speaker_layouts/allosphere_layout.json \
--positions data/processedData/stageForRender/scene.lusid.json \
--sources data/processedData/stageForRender/ \
--out render.wav
# DBAP with tight focus
./build/source/spatial_engine/spatialRender/spatialroot_spatial_render \
--spatializer dbap --dbap_focus 3.0 \
--layout translab_layout.json \
--positions scene.lusid.json \
--sources ./stageForRender/ \
--out render_tight.wav
# Debug single source
./build/source/spatial_engine/spatialRender/spatialroot_spatial_render \
--solo_source "11.1" \
--debug_dir ./debug_output/ \
--layout allosphere_layout.json \
--positions scene.lusid.json \
--sources ./stageForRender/ \
--out debug_source.wavRequired flags: --layout, --positions, --sources, --out
Spatializer: --spatializer dbap|lbap, --dbap_focus, --lbap_dispersion
General: --master_gain, --solo_source, --t0, --t1, --elevation_mode, --debug_dir
Fixed: C++ renderer now uses mSpatial.duration from LUSID scene instead of inferring from WAV file length. This prevents truncated renders when keyframes end before composition end (e.g., 9:26 ADM → correct 566s render, not truncated 167s).
Problem: Standard WAV 32-bit data-chunk size wraps at 4 GB. 56-channel × 566s × 48kHz × 4B = 5.67 GB caused header overflow, making readers report ~166s instead of 566s. Audio data on disk was correct — only the header was wrong.
Fix: WavUtils::writeMultichannelWav() auto-selects SF_FORMAT_RF64 when audio data exceeds 4 GB. RF64 (EBU Tech 3306) uses 64-bit size fields. Falls back to standard WAV for files under 4 GB.
Duration limits at 48 kHz 32-bit float: 56-channel layout → ~6.6 min before RF64 kicks in.
Default: RescaleAtmosUp — maps Atmos-style elevations [0°, +90°] into the layout's actual elevation range. Prevents sources from becoming inaudible at zenith.
| Mode | CLI | Description |
|---|---|---|
RescaleAtmosUp |
--elevation_mode rescale_atmos_up |
Default. Maps [0°, +90°] → layout range |
RescaleFullSphere |
--elevation_mode compress |
Maps full [-90°, +90°] range |
Clamp |
--no-vertical-compensation |
Hard clip |
- Sources named
"LFE"or node typeLFEbypass spatialization. - Routed directly to compact internal subwoofer channels, then scattered to the layout-defined subwoofer
deviceChanneloutputs. - Energy divided by number of subs.
- Gain compensation:
dbap_sub_compensation = 0.95(global — TODO: make configurable). - Output buffer auto-sized to
max(deviceChannel) + 1across both speakers and subwoofers.
{
"speakers": [...],
"subwoofers": [{ "channel": 16 }, { "channel": 17 }]
}Zero-Block Detection & Fallback:
- Detects when spatializer produces silence despite input energy
- Fallback: retarget direction 90% toward nearest speaker
- Threshold:
kPannerZeroThreshold = 1e-6
Fast-Mover Sub-Stepping:
- Detects sources moving >14° (~0.25 rad) within a 64-sample block
- Subdivides block into 16-sample chunks with per-chunk direction
- Prevents "blinking" artifacts from rapid trajectory changes
Direction Validation:
- NaN/Inf check on all directions before use
- Zero-length vectors → front
[0, 1, 0] - Warnings rate-limited (once per source, not per block)
AlloLib DBAP applies internal transform (x,y,z) → (x,-z,y). directionToDBAPPosition() in SpatialRenderer.cpp compensates automatically — no action needed from callers.
End-of-render diagnostics (--debug_dir writes render_stats.json, block_stats.log):
- Overall peak, near-silent channels, clipping channels, NaN channels
- Direction sanitization summary (clamped/rescaled/invalid counts)
- Panner robustness summary (zero-blocks, retargets, sub-stepped blocks)
source/spatial_engine/spatialRender/SpatialRenderer.cpp/.hpp— core renderersource/spatial_engine/spatialRender/OfflineOutputRouteMap.cpp/.hpp— offline compact-bus to sparse-output routingsource/spatial_engine/src/JSONLoader.cpp/.hpp— LUSID scene parsersource/spatial_engine/src/LayoutLoader.cpp/.hpp— speaker layout parsersource/spatial_engine/src/WavUtils.cpp/.hpp— WAV/RF64 I/O
Ownership boundary:
source/spatial_engine/realtimeEngine/owns the live playback enginesource/spatial_engine/spatialRender/owns offline renderingsource/spatial_engine/src/owns shared loaders/utilities used by both
Interpolation: Block-center SLERP for direction between LUSID keyframes.
Safe Fallback: Last-good direction → nearest keyframe → front (0,1,0).
LUSID Scene Parser (JSONLoader.cpp):
JSONLoader::loadLusidScene(path)→SpatialDatastruct- Extracts
audio_object,direct_speaker,LFEnodes - Converts timestamps using
timeUnit+sampleRate - Source keys use node ID format (
"1.1","11.1") - Ignores
spectral_features,agent_statenodes
DBAP-Testing.md— Field testing at Translab (Feb 3–10, 2026) with "Swale", 8.8.2 speaker config (2 subs).
| Focus | Observation |
|---|---|
| 1.0 | Best level balance, not very localized |
| 1.5 | Sweet spot — localized but slightly dispersed. Preferred listening target from field tests. |
| 2.0 | Strong localization but level adjustment needed |
| 2.5 | Mix becomes muddy |
- Realtime
EngineSessiondefault focus is 1.5 viaRuntimeParams::defaults() - Offline renderer CLI default
--dbap_focusis currently 1.0 in code - Use an explicit
--dbap_focusvalue when parity between tools matters - Conduct further testing for range 1.1–1.5
- Subs need energy distribution based on number of subs — currently routed equally to both
- Sub level at focus 2.0 is 30–40% too loud (DBAP focus increases energy concentration → mains drop, but LFE bypass means subs don't compensate)
- Possible future: scale sub level based on DBAP focus parameter if listening tests show a persistent main/sub balance shift. This is separate from DBAP normalization; the old
focusAutoCompensationpath has been removed.
Offline renderer master gain defaults to 0.0 dB unity gain (RenderConfig::masterGainDb = 0.0f in spatialRender/SpatialRenderer.hpp; --master_gain help in spatialRender/main.cpp). Realtime RuntimeParams::masterGainDb also defaults to 0.0f.