- C++ 85.9%
- Python 11.4%
- CMake 2.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| assets | ||
| docs | ||
| examples | ||
| include/testbed | ||
| src | ||
| tests | ||
| tools | ||
| .gitignore | ||
| CMakeLists.txt | ||
| README.md | ||
| vcpkg.json | ||
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 elevationinput mouse-sensitivity value/input gamepad-deadzone valuecrouch 0|1— crouch collision shape, with overhead-clearance rejectiontuning acceleration|air|friction|jump|step|slope value— Jolt movement tuningsdoor name x y z hx hy hz dx dy dz speed— kinematic sliding boxdoors,doorstate id 0|1,doorremove id,interact— interact with doorssimulate seconds— advance gameplay and door transforms deterministicallyannounce "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.