No description
  • C++ 85.9%
  • Python 11.4%
  • CMake 2.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-12 08:41:20 +02:00
assets Bootstrap C++ acoustics testbed with OpenAL, Assimp, SDL3, and scripting 2026-10-12 03:00:54 +02:00
docs Add tolerant structured comparison of experimental snapshots 2026-10-12 08:41:20 +02:00
examples Add fixed-tick headless experiment scheduler and validation tests 2026-10-12 08:40:11 +02:00
include/testbed Remove world rendering and harden GUI authoring, physics and persistence 2026-10-12 08:19:16 +02:00
src Remove world rendering and harden GUI authoring, physics and persistence 2026-10-12 08:19:16 +02:00
tests Add tolerant structured comparison of experimental snapshots 2026-10-12 08:41:20 +02:00
tools Add tolerant structured comparison of experimental snapshots 2026-10-12 08:41:20 +02:00
.gitignore Add tolerant structured comparison of experimental snapshots 2026-10-12 08:41:20 +02:00
CMakeLists.txt Add tolerant structured comparison of experimental snapshots 2026-10-12 08:41:20 +02:00
README.md Remove world rendering and harden GUI authoring, physics and persistence 2026-10-12 08:19:16 +02:00
vcpkg.json Integrate Glaze scene formats, Quake BSP and PAK imports, and Jolt navigation 2026-10-12 04:05:02 +02:00

Acoustics Testbed

C++23 CMake / vcpkg spatial audio and geometric-acoustics harness.

Current architecture

  • Glaze replaces nlohmann-json for scene presets. Supported input extensions include JSON, TOML, YAML, BEVE, CBOR, MessagePack, BSON and JSONB.
  • Native versioned scene documents (editable JSON, compact BEVE) define asset IDs, external geometry URIs, explicit interactive doors, emitters and physics settings. See scene format.
  • Assimp meshes, Quake 1 BSP29, Quake II IBSP38 and Quake 3 BSP46 triangulations load into common acoustic/collision geometry. Quake 1 PAK directories can be enumerated and extracted.
  • Jolt reconstructs a static collision mesh on geometry changes and runs a capsule CharacterVirtual controller with gravity, support queries, obstacle collision and stepped fixed-timestep movement. This does not yet implement dynamic moving doors.
  • TGUI SDL3 is declared in the vcpkg manifest, but a complete accessible TGUI editor with Prism announcements and widget focus semantics has not yet been implemented.
  • Quake rendering (textures/lightmaps/shaders) and original engine gameplay remain separate from the acoustic geometry importer. These are not a Quake engine emulator.

This repository deliberately does not implement reflections, occlusion, propagation, or acoustic material estimation. Those are delegated to the AcousticsAdapter boundary in include/testbed/runtime.hpp. OpenAL is currently a dry 3D playback backend, not an acoustic simulation engine.

Build

Use vcpkg manifest mode with the CMake toolchain. Both TGUI and Jolt dependencies are enabled by default; disable either with -DACOUSTICS_WITH_TGUI=OFF or -DACOUSTICS_WITH_JOLT=OFF when testing the independent core:

cmake -S . -B build -G Ninja -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake
cmake --build build
ctest --test-dir build --output-on-failure

Run: ./build/acoustics-testbed or ./build/acoustics-testbed --headless --script examples/automated.txt. The example JSON preset creates four spatial tones with no external audio files. Use preset examples/four-corners.json interactively.

Commands are available from the SDL window with :; press Enter to execute and Escape to cancel. Diagnostic output goes to the terminal (no graphical renderer required). Use help for the complete command reference. Commands in headless script files execute one per line; use sleep seconds for timed playback.

The SDL window is deliberately minimal and does not depict 3D geometry. Screen-reader users can drive it by commands via standard input in headless mode, or via SDL keyboard shortcuts in interactive mode. A fully accessible native command UI is an outstanding enhancement; SDL's text input alone does not provide screen-reader announcements.

Design conventions

  • Coordinate system: X east, Y up, Z south; units meters; forward faces -Z at yaw=0.
  • Geometry import flattens Assimp's scene graph, applying all node transforms.
  • Acoustic material values are placeholders, never guessed from visual materials.
  • OpenAL buffers loaded via libsndfile and downmixed to mono signed 16-bit PCM for true positional audio. This is intended for mono object sources, not ambisonic beds.
  • All playback uses the default OpenAL device. Headless mode still requires a working OpenAL device (OpenAL Soft null backend may be configured externally for CI).
  • Source clips load completely into memory. Large recording streaming and ambisonic playback are follow-up items.
  • Errors for OpenAL commands and ALC initialization are checked and propagated as C++ exceptions.
  • AcousticsAdapter intentionally does not own the OpenAL context.

Headless collision navigation and Quake PAK examples

room 20 4 20
listener 10 1.7 10
walk 1 0 1
grounding
paklist "id1/pak0.pak"
pakextract "id1/pak0.pak" "maps/e1m1.bsp" "e1m1.bsp"
load "e1m1.bsp"

Quake 1 BSP29, Quake II IBSP38 and Quake 3 BSP46 are decoded by separate importers. The present collision mesh uses triangulated render faces; it is not identical to Quake's BSP collision hulls. The import scale defaults to 0.0254 meters per Quake unit and should be explicitly calibrated to the selected game's assets. A mesh file does not intrinsically identify a door. Quake entities and authored native-scene references can supply that metadata, but door hinges/constraints must be linked to actual moving geometry before they operate.

Tests

Run ctest --test-dir build --output-on-failure. The regression suite covers procedural meshes, OBJ import, Q3 BSP metadata and tessellation, Quake 1 BSP29 face reconstruction, PAK entries, Glaze preset parsing, native JSON/BEVE roundtrips, and Jolt static-body/character behavior when enabled.

Audit a Quake map archive

The acoustics-map-audit executable scans BSP29, IBSP38 and IBSP46 maps without initializing SDL, OpenAL or Jolt. It validates indexed geometry, reports triangle and material counts with bounds in meters, and returns an error code when imported geometry is rejected. Example:

./build/acoustics-map-audit --scale 0.0254 ~/AudioQuakeTesting

The archive may contain non-BSP29/38/46 variants and corrupted map files. Those are reported separately; no unchecked pointers or face records are passed into the physics system. Quake's source unit is not one physical meter. The scale is meters per native map coordinate unit; 0.0254 assumes one Quake unit equals one inch. A 72-unit height then becomes roughly 1.83 m. Choosing 1.0 would make that height 72 meters, not normalize the character or room to human proportions.

Brush models and acoustic geometry identity

BSP29, IBSP38 and IBSP46 face triangles retain their source brush-model ownership. Quake 1 also exposes authored brush-model records and entity links via models after load. Dynamic bodies/hinges are not yet created; currently all reconstructed triangles participate in the same static Jolt mesh. The grouping is preserved so that subsequent physics and acoustics instance construction can make doors and platforms genuinely independent.

A Glaze scene preset may set meters_per_unit explicitly. Without it, BSP files default to 0.0254 and non-BSP Assimp assets default to 1.0.

First-person navigation controls (Jolt build)

The SDL window is a keyboard-driven audio-first first-person viewport; there is no 3D picture rendering. Hold W/S to walk forward/backward, A/D to strafe, Shift to sprint, and press Space to jump. Left/right arrow keys turn. The physics controller runs at 60 Hz with Jolt gravity, collision and grounded state; movement is disabled while the command prompt is active. The window remains an early SDL shell, not yet the accessible TGUI/Prism editor.

Headless scripts and the in-window command prompt can use look <degrees> (relative yaw), move <forward-mps> <right-mps> <seconds>, jump, grounding, and walk <world-vx> <world-vz> <seconds>. For example:

room 20 4 20
listener 10 1.7 10
look 90
move 3 0 1
grounding
jump
move 0 0 0.2
grounding

In the SDL window, Space is reserved for character jumping when Jolt is active. Audio-source pausing remains available through the pause <id> command. Non-Jolt builds retain manual translation controls.

QuakeC/Quake II game logic and QVM support are not present. Future scripting should emit semantic scene actions (open door, activate trigger, spawn emitter) through an independently authored runtime interface rather than expose raw engine internals or assume original game physics.

Accessible audio-first FPS and scene editor

The SDL3 application uses Jolt CharacterVirtual and operates in physical meters; it is not a rendered Quake FPS. Movement uses 60 Hz fixed ticks, gravity, ground acceleration/friction, air acceleration, 0.35 m stair stepping, slope support and collision-checked crouching. The listener follows the physical character. Controls in FPS mode: W/S forward/back, A/D strafe, Shift run, Ctrl crouch, Space jump, mouse for yaw/pitch, E interact, F2 to return to editor. SDL3 gamepads use the left analog stick for walking, the right for looking, South for jumping, and East for crouching. Look pitch is clamped to +/-85 degrees. The mouse is captured only in FPS mode; the accessible editor retains ordinary Tab/Shift+Tab navigation.

With ACOUSTICS_WITH_TGUI=ON, the SDL3-renderer TGUI interface includes accessible scene command input, scene status and feedback, source and material lists, editable three-band acoustic absorption and scattering, door lists, and buttons for creation, playback, physics, snapshots, and interaction. It uses native TGUI widget accessibility metadata and its AccessKit backend when available; it does not substitute custom speech for semantic UI roles. The optional ACOUSTICS_WITH_PRISM module dynamically locates Prism and can announce command feedback when a suitable backend is installed. Prism is not required to compile or navigate TGUI controls. Real platform screen-reader verification is still necessary, separately from dummy-SDL testing.

The following commands supplement the existing room, load, preset, snapshot, probe, move, walk, and audio-source controls:

  • viewpitch degrees / lookpitch relative-degrees — FPS look elevation
  • input mouse-sensitivity value / input gamepad-deadzone value
  • crouch 0|1 — crouch collision shape, with overhead-clearance rejection
  • tuning acceleration|air|friction|jump|step|slope value — Jolt movement tunings
  • door name x y z hx hy hz dx dy dz speed — kinematic sliding box
  • doors, doorstate id 0|1, doorremove id, interact — interact with doors
  • simulate seconds — advance gameplay and door transforms deterministically
  • announce "message" — Prism speech, when backend is available

Kinematic doors have a dedicated Jolt actor and their own local acoustic triangles. Updates are forwarded as AcousticsAdapter::dynamic_geometry_changed with an actor ID and world transform, and removals via dynamic_geometry_removed. For imported Quake BSP29 files, only closed, convex func_door/func_plat submodels that pass conservative validation are promoted to kinematic instances; other submodels remain static to preserve collisions. This is deliberately not Quake VM compatibility and does not pretend to implement all trigger, path, multiplayer or game rules.

Claude control and audio capture

Launch the machine-readable controller with --jsonl --loopback, using one JSON request per standard input line, with id and command:

{"id":"room1","command":"room 20 4 20"}
{"id":"player","command":"listener 10 1.7 10"}
{"id":"state","command":"snapshot"}
{"id":"move","command":"move 3 0 1"}
{"id":"ray","command":"probe 10 1 10 10 -1 10"}
{"id":"wav","command":"record /tmp/testbed.wav 2"}

Each command yields one response like:

{"type":"command_result","id":"room1","ok":true,"output":"Created room with 12 triangles\n","error":""}

Errors set ok=false and populate error. snapshot returns structured Glaze JSON for listener/character/geometry/doors, currently nested as text in output. probe yields JSON for Jolt ray hit, distance, actor and position. All commands use the same simulation implementation as the editor. There is no unauthenticated network listener or guessed proprietary Claude endpoint.

--loopback uses OpenAL Soft's ALC_SOFT_loopback device to render real 48 kHz stereo PCM16 mixed audio. record output.wav seconds writes this to disk; no sound card or microphone is required. Capturing advances the physics and scene adapter at fixed 60 Hz sample-aligned blocks, so recorded timelines can cover door motion and changing source/listener state. This is the current OpenAL mixer's output, not evidence of Steam Audio propagation, IR convolution, reflections or calibrated acoustics, since a Steam Audio renderer is not yet connected. Audio output should not be described as Steam Audio results until that backend, dynamic instances and its convolution path have been verified.

Still needed for production quality

The generic native scene JSON/BEVE files have not yet been fully connected to runtime asset/node instances. Complete accessible scene-tree editing, inspector undo/redo, microphone/input selection, native file pickers and screen-reader tests remain. Quake maps can contain complex nonconvex brush aggregations, trigger/game logic, liquid/ladder traversal, and platforms with compound collision; current importer implements a conservative subset only. Accurate Steam Audio physical propagation and IR baking/realtime mixing remain separate backend implementation tasks. QuakeC, Quake II game modules, and QVM execution are expressly deferred.

Scene persistence and reversible acoustic edits

save scene.json persists the current imported asset or authored room, listener pose, sound-source references or generated tones, acoustic material coefficients, and user-authored sliding boxes. preset scene.json loads those settings again. BEVE output is available as save scene.beve; the pre-existing Glaze readers accept the other supported interchange formats. Saves use a temporary file and rename so a failed write does not leave a truncated scene. undo and redo reverse material coefficients, audio-source properties and positions, and door state edits. Their history is bounded to 512 commands; creation and deletion still require dedicated object identity restoration. Quake BSP convex-platform instances are regenerated from the imported map and are not serialized as manually created boxes.

The Quake 1 BSP29 loader now prefers info_player_start and falls back to info_player_deathmatch for an initial physical character spawn, using the same metric Y-up conversion as imported surfaces and accounting for the original player hull's lower extent. It does not yet reproduce Quake's full spawn-selection/game rules.

The runtime-control CTest integration test launches the headless JSONL controller, applies reversible scene edits, opens and moves a Jolt door, records non-silent 48 kHz PCM16 output, verifies that recording advances physics time, and reloads the serialized scene in a fresh process.

Native Quake triggers and moving platforms (limited compatibility)

Brush-submodel triggers and textual trigger_once/trigger_multiple bounding boxes can now activate native kinematic doors through Quake target and targetname relationships. This handles a conservative subset of original world interactions without executing QuakeC. Map-authored trigger volumes are regenerated from the BSP on load; user-authored triggers and doorlink bindings are stored in scene presets without duplicating imported triggers. Timed doors may automatically close. Commands target, trigger, triggers, and doorlink support manual experiments.

Moving platforms use Jolt's MoveKinematic with a real velocity supplied to CharacterVirtual; the controller inherits ground-body velocity so grounded characters can ride horizontally moving platforms. A physics regression checks rider displacement. The implementation is not a full Quake mover or trigger VM: ladders, liquids, scripted trigger chains, audio cues and precise Quake spawn/game rules require further work.

Reversible object edits and structured diagnostics

Source and kinematic-door creation/deletion operations can be undone and redone with stable logical IDs, preserving sound buffers across source restoration. The scene editor has an audio-source inspector, a trigger list, and native accessible TGUI widget descriptions; keyboard input in editor mode is isolated from FPS controls. tree emits a JSON scene inventory across geometry, sources, materials, doors and triggers. wavstats measures overall and per-channel RMS/peak, sample count and duration from an audio file; assertdoor and assertlistener fail scenarios with explicit diagnostics. The JSONL integration test covers these operations along with 60 Hz sample-aligned loopback capture.

Authored ladder and water navigation volumes

The scene runtime can add, enumerate, remove and restore two forms of physics-navigation region with an axis-aligned metric bounding box:

volume ladder access-ladder 4 0 4 6 4 6
volume water pool 7 0 7 9 4 9
volumes
climb 1.5 0.5
swim 1.0 0.5

Climbing inside a ladder volume uses gravity-free vertical motion through the existing Jolt CharacterVirtual collision solver. Swimming through a water volume slows horizontal movement, reduces effective gravity and permits controlled ascent/descent, while leaving world collision intact. In FPS mode, W/S or Space/Ctrl supply ladder ascent/descent; Space/Ctrl control underwater height. Script commands climb and swim apply a chosen meters-per-second vertical velocity for a deterministic interval, rejecting attempts made outside the corresponding region. These are native testbed behaviors, not a claim of exact Quake liquid, ladder or buoyancy simulation. Volume objects have stable IDs, can be undone/redone, appear in the JSON scene tree, and are serialized into the JSON/BEVE scene preset. They do not implicitly alter sound propagation; any acoustic changes remain the renderer's responsibility.

The native trigger registry also supports triggerremove/triggerrestore; trigger indexes remain stable as tombstones during undo/redo so object references do not silently shift. Saved presets exclude removed and BSP-imported triggers (the latter are recreated on BSP load).

Live scene-node hierarchy (incremental)

The command controller now supports parented scene nodes alongside the legacy flat objects, with commands nodecreate, nodeparent, nodemove, noderemove, nodebind, nodeunbind, and nodeworld. Group nodes can hierarchically transform emitter nodes; bound emitters update the live OpenAL source position (and send the same source position update to the acoustics adapter). A node's parent changes its world transform without rewriting its local transform. Node creation, reparenting, movement, and binding are reversible using the existing bounded undo and redo commands. Save/reload persists the hierarchy and maps source bindings by saved source ordinal rather than volatile OpenAL object IDs. This is a limited integration: imported mesh instances and door actors are not yet bound to arbitrary hierarchy nodes, and node world position changes do not imply authored geometry deformation.

Example:

nodecreate world / group 0 0 0
nodecreate sound world emitter 4 1 2
nodebind sound 1
nodemove world 2 0 0
nodeworld sound
undo

Hierarchy nodes support noderotate id qx qy qz qw and nodescale id sx sy sz. Both use the same bounded undo/redo history and synchronize linked sound sources. The native hierarchy composes local translation, rotation, and scale through parent chains using a full affine basis, including cases where nonuniform parent scaling and descendant rotations introduce shear. Deleting a leaf node retains its complete snapshot, including quaternion and scale, for noderestore and undo. Bound sound sources cannot be deleted or independently repositioned until nodeunbind is used; this avoids divergent scene/world transforms.

With TGUI enabled, the scene editor exposes a node list with parent labels, local-position and parent inputs, and Move/Reparent/Delete actions. More complex asset and door instance bindings are not yet implemented by the live hierarchy, even though the native scene file format can represent them.

Integrated non-acoustic world authoring and first-person preview

The interactive editor's scene-node inspector can now edit local translation, quaternion rotation, and scale, create groups/emitters/door nodes, import mesh assets, bind selected audio sources or kinematic doors, remove nodes, and undo and redo a grouped edit. The node list indents each item by parent depth and tries to preserve selection when a command refreshes it. Node names and controls are explicitly named in TGUI's accessibility model; actual assistive technology validation is intentionally reserved for the user.

FPS/navigation mode remains audio-first, with no 3D world rendering. SDL3 draws only TGUI editor controls; geometry meshes are retained for collision, scene authoring, inspection and later acoustic processing.

Mesh instances and hierarchical physics

Use nodeasset to load an Assimp-supported mesh as an independent asset instance. Each enabled instance contributes fully transformed world triangles (and its own material indices) to Jolt static collision. Moving, scaling, rotating, or reparenting its node rebuilds the composed collision geometry, while preserving the underlying authored map. assetvisible disables both scene/collision contributions and is undoable. The asset file path, node graph and active state round-trip through JSON/BEVE presets. Presets also persist authored door-node links by stable saved ordinal, then remap them to the actual Jolt actor IDs after loading. Scene opening replaces existing audio sources instead of duplicating them on each reload.

room 20 5 20
nodecreate structure / group 0 0 0
nodeasset column structure "assets/column.obj" 10 0 10
nodeinspect column
assertprobe 1 8 1 10 12 1 10
nodemove structure 2 0 0
assetvisible column 0
undo
save "scene.json"

For deterministic headless scenarios, nodeinspect emits local transforms, parent ID, world position and optional bound source/door IDs as structured JSON. tree includes parent links, asset IDs and live object bindings. assertprobe reports an explicit test error when a physics raycast does not match the expected hit state. Headless walk and move now advance the same 60 Hz world update as the interactive runtime, so door timers, trigger targets, player collisions and actor transforms progress consistently during scripted traversal.

Grouped history

txbegin, txcommit and txcancel group consecutive reversible commands into a single editor undo step. Cancelling replays inverses in reverse order. Failed transaction replays attempt compensating inverses for already applied actions. Scene replacement is prohibited while a transaction is open. Grouped edits do not claim to provide a crash-consistent filesystem transaction: externally visible operations such as saving WAV files or loading arbitrary assets are not themselves guaranteed atomic. Steam Audio implementation, legacy Quake bytecode runtimes and manual screen-reader testing remain explicitly deferred.

Audio-first editor hardening

The SDL3/TGUI interface is the only graphical output. The former 3D world preview, depth rasterizer, texture upload and visual renderer tests have been removed; the FPS/navigation mode retains the first-person physics controller, spatial sound and input handling without drawing world triangles. Imported meshes are retained for authoring, Jolt collisions and acoustic geometry.

The authoring GUI now supports renaming and duplicating single scene nodes, without implicitly copying a bound OpenAL source or kinematic door. Renaming preserves descendant parent references and live actor/source bindings; duplicating an asset node shares the immutable imported mesh cache while creating a distinct transform and collision instance. For native navigation volumes and triggers, accessible creation and deletion controls are included. The noderename old new and nodeclone source new commands are undoable. Duplication is deliberately one node at a time, not an automatic recursive subtree clone. For compound edits, use txbegin and txcommit.

editor-hardening runs isolated end-to-end JSONL sessions against invalid hierarchy operations, malformed source files, failed saves, non-mutating command errors, grouped undo/redo and cancel, repeated scene reload, and a bounded 250-node plus 8 simulated-second workload. These checks are intended to detect regressions or hangs, not to guarantee frame-time or production service-level performance. Jolt tests also include 500 deterministic kinematic actor teleports, raycast broadphase correctness and invalid motion validation. Manual screen-reader interoperability testing remains reserved for the user.