@woosh/meep-engine 2.154.0 → 2.156.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/build/bundle-worker-image-decoder.js +1 -1
- package/build/bundle-worker-terrain.js +1 -1
- package/editor/view/ecs/ComponentControlView.d.ts +0 -9
- package/editor/view/ecs/ComponentControlView.js +2 -98
- package/package.json +1 -1
- package/src/core/binary/32BitEncoder.js +1 -1
- package/src/core/binary/to_half_float_uint16.js +3 -3
- package/src/core/bvh2/bvh3/ebvh_build_hierarchy_radix.d.ts.map +1 -1
- package/src/core/bvh2/bvh3/ebvh_build_hierarchy_radix.js +275 -253
- package/src/core/cache/Cache.d.ts.map +1 -1
- package/src/core/cache/Cache.js +7 -0
- package/src/core/cache/FrequencySketch.d.ts.map +1 -1
- package/src/core/cache/FrequencySketch.js +8 -4
- package/src/core/clipboard/obtainClipBoard.d.ts +6 -0
- package/src/core/clipboard/obtainClipBoard.d.ts.map +1 -0
- package/src/core/clipboard/obtainClipBoard.js +29 -0
- package/src/core/clipboard/safeClipboardReadText.d.ts +6 -0
- package/src/core/clipboard/safeClipboardReadText.d.ts.map +1 -0
- package/src/core/clipboard/safeClipboardReadText.js +55 -0
- package/src/core/clipboard/safeClipboardWriteText.d.ts +8 -0
- package/src/core/clipboard/safeClipboardWriteText.d.ts.map +1 -0
- package/src/core/clipboard/safeClipboardWriteText.js +23 -0
- package/src/core/collection/array/array_quick_sort_by_lookup_map.js +1 -1
- package/src/core/collection/array/array_set_diff_sorting.d.ts.map +1 -1
- package/src/core/collection/array/array_set_diff_sorting.js +4 -1
- package/src/core/collection/array/array_shuffle.d.ts.map +1 -1
- package/src/core/collection/array/array_shuffle.js +30 -27
- package/src/core/collection/array/binarySearchLowIndex.d.ts.map +1 -1
- package/src/core/collection/array/binarySearchLowIndex.js +4 -3
- package/src/core/collection/array/typed/array_buffer_hash.js +1 -1
- package/src/core/collection/array/typed/is_typed_array_equals.d.ts.map +1 -1
- package/src/core/collection/array/typed/is_typed_array_equals.js +12 -2
- package/src/core/collection/heap/BinaryHeap.d.ts.map +1 -1
- package/src/core/collection/heap/BinaryHeap.js +12 -2
- package/src/core/collection/queue/Deque.d.ts.map +1 -1
- package/src/core/collection/queue/Deque.js +10 -8
- package/src/core/collection/table/RowFirstTable.d.ts.map +1 -1
- package/src/core/collection/table/RowFirstTable.js +4 -2
- package/src/core/collection/table/RowFirstTableSpec.js +2 -2
- package/src/core/color/operations/color_lerp.d.ts.map +1 -1
- package/src/core/color/operations/color_lerp.js +10 -3
- package/src/core/color/rgb2uint32.js +1 -1
- package/src/core/color/rgbe9995_to_rgb.js +1 -1
- package/src/core/function/objectsEqual.d.ts.map +1 -1
- package/src/core/function/objectsEqual.js +2 -1
- package/src/core/geom/2d/aabb/AABB2.d.ts.map +1 -1
- package/src/core/geom/2d/aabb/AABB2.js +12 -11
- package/src/core/geom/2d/convex-hull/convex_hull_jarvis_2d.d.ts.map +1 -1
- package/src/core/geom/2d/convex-hull/convex_hull_jarvis_2d.js +30 -4
- package/src/core/geom/2d/convex-hull/fixed_convex_hull_relaxation.d.ts.map +1 -1
- package/src/core/geom/2d/convex-hull/fixed_convex_hull_relaxation.js +6 -2
- package/src/core/geom/2d/hash-grid/SpatialHashGrid.d.ts.map +1 -1
- package/src/core/geom/2d/hash-grid/SpatialHashGrid.js +388 -386
- package/src/core/geom/2d/hash-grid/shg_query_elements_line.d.ts.map +1 -1
- package/src/core/geom/2d/hash-grid/shg_query_elements_line.js +8 -3
- package/src/core/geom/2d/quad-tree/QuadTreeDatum.d.ts.map +1 -1
- package/src/core/geom/2d/quad-tree/QuadTreeDatum.js +9 -1
- package/src/core/geom/2d/quad-tree/qt_query_data_nearest_to_point.d.ts +3 -1
- package/src/core/geom/2d/quad-tree/qt_query_data_nearest_to_point.d.ts.map +1 -1
- package/src/core/geom/2d/quad-tree/qt_query_data_nearest_to_point.js +3 -1
- package/src/core/geom/2d/quad-tree-binary/QuadTree.js +714 -714
- package/src/core/geom/2d/r-tree/StaticR2Tree.d.ts.map +1 -1
- package/src/core/geom/2d/r-tree/StaticR2Tree.js +5 -4
- package/src/core/geom/3d/aabb/aabb3_detailed_volume_intersection.d.ts.map +1 -1
- package/src/core/geom/3d/aabb/aabb3_detailed_volume_intersection.js +33 -29
- package/src/core/geom/3d/aabb/aabb3_near_distance_to_intersection_ray_segment.d.ts.map +1 -1
- package/src/core/geom/3d/aabb/aabb3_near_distance_to_intersection_ray_segment.js +3 -1
- package/src/core/geom/3d/aabb/aabb3_signed_distance_to_aabb3.d.ts.map +1 -1
- package/src/core/geom/3d/aabb/aabb3_signed_distance_to_aabb3.js +10 -7
- package/src/core/geom/3d/aabb/aabb3_transformed_compute_plane_side.d.ts.map +1 -1
- package/src/core/geom/3d/aabb/aabb3_transformed_compute_plane_side.js +30 -9
- package/src/core/geom/3d/aabb/compute_aabb_from_points.js +3 -3
- package/src/core/geom/3d/box/box3_raycast.d.ts +37 -0
- package/src/core/geom/3d/box/box3_raycast.d.ts.map +1 -0
- package/src/core/geom/3d/box/box3_raycast.js +81 -0
- package/src/core/geom/3d/capsule/capsule_raycast.d.ts +35 -0
- package/src/core/geom/3d/capsule/capsule_raycast.d.ts.map +1 -0
- package/src/core/geom/3d/capsule/capsule_raycast.js +93 -0
- package/src/core/geom/3d/cone/compute_bounding_cone_of_2_cones.d.ts.map +1 -1
- package/src/core/geom/3d/cone/compute_bounding_cone_of_2_cones.js +4 -0
- package/src/core/geom/3d/frustum/frustum3_computeNearestPointToPoint.js +1 -1
- package/src/core/geom/3d/line/line3_compute_segment_point_distance_eikonal.d.ts.map +1 -1
- package/src/core/geom/3d/line/line3_compute_segment_point_distance_eikonal.js +3 -2
- package/src/core/geom/3d/mat4/decompose_matrix_4_array.d.ts.map +1 -1
- package/src/core/geom/3d/mat4/decompose_matrix_4_array.js +12 -2
- package/src/core/geom/3d/mat4/eulerAnglesFromMatrix.js +2 -2
- package/src/core/geom/3d/mat4/m4_multiply_alphatensor.d.ts +1 -1
- package/src/core/geom/3d/mat4/m4_multiply_alphatensor.d.ts.map +1 -1
- package/src/core/geom/3d/mat4/m4_multiply_alphatensor.js +19 -13
- package/src/core/geom/3d/octahedra/octahedral_direction_to_uv.d.ts.map +1 -1
- package/src/core/geom/3d/octahedra/octahedral_direction_to_uv.js +3 -2
- package/src/core/geom/3d/plane/plane3_compute_plane_intersection.js +3 -2
- package/src/core/geom/3d/shape/MeshShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/MeshShape3D.js +7 -0
- package/src/core/geom/3d/shape/UnionShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/UnionShape3D.js +3 -2
- package/src/core/geom/3d/shape/util/shape3d_voxelize_to_grid.d.ts.map +1 -1
- package/src/core/geom/3d/shape/util/shape3d_voxelize_to_grid.js +153 -148
- package/src/core/geom/3d/sphere/harmonics/sh3_dering_optimize_positive.d.ts.map +1 -1
- package/src/core/geom/3d/sphere/harmonics/sh3_dering_optimize_positive.js +7 -0
- package/src/core/geom/3d/sphere/harmonics/sh3_sample_by_direction.d.ts.map +1 -1
- package/src/core/geom/3d/sphere/harmonics/sh3_sample_by_direction.js +13 -10
- package/src/core/geom/3d/sphere/sphere_projected_sphere_radius_sqr.d.ts +1 -1
- package/src/core/geom/3d/sphere/sphere_projected_sphere_radius_sqr.js +2 -2
- package/src/core/geom/3d/sphere/sphere_raycast.d.ts +33 -0
- package/src/core/geom/3d/sphere/sphere_raycast.d.ts.map +1 -0
- package/src/core/geom/3d/sphere/sphere_raycast.js +47 -0
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_tet_get_neighbours.d.ts +24 -0
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_tet_get_neighbours.d.ts.map +1 -0
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_tet_get_neighbours.js +39 -0
- package/src/core/geom/3d/tetrahedra/triangle/trace_triangular_depth_map.d.ts.map +1 -1
- package/src/core/geom/3d/tetrahedra/triangle/trace_triangular_depth_map.js +4 -2
- package/src/core/geom/3d/topology/bounds/computeTriangleClusterNormalBoundingCone.js +3 -3
- package/src/core/geom/3d/topology/simplify/decimate_edge_collapse_snap.js +1 -1
- package/src/core/geom/3d/topology/tm_vertex_compute_normal.d.ts.map +1 -1
- package/src/core/geom/3d/topology/tm_vertex_compute_normal.js +4 -2
- package/src/core/geom/3d/util/make_justified_point_grid.d.ts.map +1 -1
- package/src/core/geom/3d/util/make_justified_point_grid.js +18 -10
- package/src/core/geom/ConicRay.d.ts.map +1 -1
- package/src/core/geom/ConicRay.js +11 -13
- package/src/core/geom/packing/max-rect/removeRedundantBoxes.d.ts.map +1 -1
- package/src/core/geom/packing/max-rect/removeRedundantBoxes.js +19 -4
- package/src/core/geom/vec3/v3_array_copy.d.ts +3 -3
- package/src/core/geom/vec3/v3_array_copy.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_array_copy.js +2 -2
- package/src/core/geom/vec3/v3_cross.d.ts +17 -0
- package/src/core/geom/vec3/v3_cross.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_cross.js +20 -0
- package/src/core/geom/vec3/v3_orthonormal_matrix_from_normal.d.ts.map +1 -0
- package/src/{engine/graphics/sh3/path_tracer/sampling → core/geom/vec3}/v3_orthonormal_matrix_from_normal.js +1 -1
- package/src/core/geom/vec3/v3_subtract.d.ts +16 -0
- package/src/core/geom/vec3/v3_subtract.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_subtract.js +19 -0
- package/src/core/graph/coloring/colorizeGraph.js +2 -2
- package/src/core/graph/csr/CSRGraph.d.ts.map +1 -1
- package/src/core/graph/csr/CSRGraph.js +325 -319
- package/src/core/graph/layout/CircleLayout.d.ts.map +1 -1
- package/src/core/graph/layout/CircleLayout.js +8 -6
- package/src/core/graph/metis/native/refine/compute_kway_params.d.ts.map +1 -1
- package/src/core/graph/metis/native/refine/compute_kway_params.js +139 -138
- package/src/core/graph/mn_graph_coarsen.d.ts.map +1 -1
- package/src/core/graph/mn_graph_coarsen.js +4 -2
- package/src/core/graph/v2/NodeContainer.js +7 -7
- package/src/core/localization/LocalizationEngine.js +1 -1
- package/src/core/math/bell_membership_function.d.ts.map +1 -1
- package/src/core/math/bell_membership_function.js +3 -1
- package/src/core/math/complex/complex_add.d.ts +4 -4
- package/src/core/math/complex/complex_add.d.ts.map +1 -1
- package/src/core/math/complex/complex_add.js +3 -3
- package/src/core/math/complex/complex_div.d.ts +4 -4
- package/src/core/math/complex/complex_div.d.ts.map +1 -1
- package/src/core/math/complex/complex_div.js +3 -3
- package/src/core/math/complex/complex_mul.d.ts +4 -4
- package/src/core/math/complex/complex_mul.d.ts.map +1 -1
- package/src/core/math/complex/complex_mul.js +3 -3
- package/src/core/math/complex/complex_sub.d.ts +4 -4
- package/src/core/math/complex/complex_sub.d.ts.map +1 -1
- package/src/core/math/complex/complex_sub.js +3 -3
- package/src/core/math/idct_1d.d.ts +4 -4
- package/src/core/math/idct_1d.d.ts.map +1 -1
- package/src/core/math/idct_1d.js +3 -3
- package/src/core/math/noise/create_simplex_noise_2d.d.ts.map +1 -1
- package/src/core/math/noise/create_simplex_noise_2d.js +4 -2
- package/src/core/math/noise/sdnoise.d.ts.map +1 -1
- package/src/core/math/noise/sdnoise.js +12 -9
- package/src/core/math/physics/mie/compute_bhmie_optical_properties.d.ts.map +1 -1
- package/src/core/math/physics/mie/compute_bhmie_optical_properties.js +94 -50
- package/src/core/math/physics/mie/lorenz_mie_coefs.d.ts +3 -6
- package/src/core/math/physics/mie/lorenz_mie_coefs.d.ts.map +1 -1
- package/src/core/math/physics/mie/lorenz_mie_coefs.js +180 -157
- package/src/core/math/physics/mie/mie_ab_to_optical_properties.d.ts +3 -4
- package/src/core/math/physics/mie/mie_ab_to_optical_properties.d.ts.map +1 -1
- package/src/core/math/physics/mie/mie_ab_to_optical_properties.js +47 -21
- package/src/core/math/random/randomIntegerBetween.d.ts.map +1 -1
- package/src/core/math/random/randomIntegerBetween.js +4 -1
- package/src/core/math/solveCubic.d.ts.map +1 -1
- package/src/core/math/solveCubic.js +95 -82
- package/src/core/math/spline/computeCatmullRomSplineUniformDistance.d.ts.map +1 -1
- package/src/core/math/spline/computeCatmullRomSplineUniformDistance.js +13 -0
- package/src/core/math/statistics/softmax.js +1 -1
- package/src/core/model/node-graph/visual/NodeGraphVisualData.d.ts +1 -0
- package/src/core/model/node-graph/visual/NodeGraphVisualData.d.ts.map +1 -1
- package/src/core/model/node-graph/visual/NodeGraphVisualData.js +2 -1
- package/src/core/model/node-graph/visual/NodeVisualData.js +1 -1
- package/src/core/model/object/ImmutableObjectPool.d.ts +7 -0
- package/src/core/model/object/ImmutableObjectPool.d.ts.map +1 -1
- package/src/core/model/object/ImmutableObjectPool.js +20 -10
- package/src/core/model/reactive/evaluation/MultiPredicateEvaluator.d.ts.map +1 -1
- package/src/core/model/reactive/evaluation/MultiPredicateEvaluator.js +39 -2
- package/src/core/model/reactive/model/terminal/ReactiveReference.d.ts.map +1 -1
- package/src/core/model/reactive/model/terminal/ReactiveReference.js +2 -0
- package/src/core/parser/simple/readHexToken.d.ts.map +1 -1
- package/src/core/parser/simple/readHexToken.js +6 -0
- package/src/core/primitives/numbers/number_pretty_print.d.ts.map +1 -1
- package/src/core/primitives/numbers/number_pretty_print.js +4 -1
- package/src/core/primitives/strings/string_jaro_winkler.js +1 -1
- package/src/core/process/CompositeProcess.js +1 -1
- package/src/core/process/action/AsynchronousDelayAction.d.ts.map +1 -1
- package/src/core/process/action/AsynchronousDelayAction.js +3 -0
- package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
- package/src/core/process/executor/ConcurrentExecutor.js +3 -2
- package/src/core/process/task/util/randomCountTask.d.ts.map +1 -1
- package/src/core/process/task/util/randomCountTask.js +3 -1
- package/src/core/process/undo/ActionProcessor.d.ts.map +1 -1
- package/src/core/process/undo/ActionProcessor.js +5 -3
- package/src/core/process/worker/WorkerBuilder.js +3 -3
- package/src/engine/animation/curve/AnimationCurve.d.ts.map +1 -1
- package/src/engine/animation/curve/AnimationCurve.js +4 -2
- package/src/engine/control/first-person/DESIGN.md +1 -1
- package/src/engine/control/first-person/FirstPersonMotionPhase.d.ts +55 -0
- package/src/engine/control/first-person/FirstPersonMotionPhase.d.ts.map +1 -0
- package/src/engine/control/first-person/FirstPersonMotionPhase.js +134 -0
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +23 -2
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerController.js +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +168 -0
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +115 -0
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +71 -0
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +255 -55
- package/src/engine/control/first-person/abilities/LedgeGrab.d.ts +82 -43
- package/src/engine/control/first-person/abilities/LedgeGrab.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/LedgeGrab.js +405 -213
- package/src/engine/control/first-person/abilities/Mantle.d.ts +6 -0
- package/src/engine/control/first-person/abilities/Mantle.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/Mantle.js +104 -45
- package/src/engine/control/first-person/abilities/ScrambleUp.d.ts +61 -0
- package/src/engine/control/first-person/abilities/ScrambleUp.d.ts.map +1 -0
- package/src/engine/control/first-person/abilities/ScrambleUp.js +182 -0
- package/src/engine/control/first-person/math/jumpDynamics.d.ts +84 -0
- package/src/engine/control/first-person/math/jumpDynamics.d.ts.map +1 -0
- package/src/engine/control/first-person/math/jumpDynamics.js +108 -0
- package/src/engine/control/first-person/prototype_first_person_controller.js +45 -1
- package/src/engine/graphics/camera/testClippingPlaneComputation.js +1 -1
- package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +8 -0
- package/src/engine/graphics/ecs/path/tube/prototypeAnimatedPathMask.js +1 -1
- package/src/engine/graphics/particles/particular/engine/utils/volume/prototypeParticleVolume.js +1 -1
- package/src/engine/graphics/render/buffer/buffers/prototypeNormalFrameBuffer.js +1 -1
- package/src/engine/graphics/render/forward_plus/plugin/ptototypeFPPlugin.js +1 -1
- package/src/engine/graphics/render/visibility/hiz/prototypeHiZ.js +1 -1
- package/src/engine/graphics/sh3/path_tracer/texture/sample_material.js +1 -1
- package/src/engine/graphics/shadows/testShadowMapRendering.js +1 -1
- package/src/engine/physics/CONSTRAINT_SOLVER_BENCH_LOG.md +208 -0
- package/src/engine/physics/CONSTRAINT_SOLVER_IMPROVEMENTS_PLAN.md +364 -0
- package/src/engine/physics/PLAN.md +6 -5
- package/src/engine/physics/constraint/solve_constraints.d.ts +4 -1
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
- package/src/engine/physics/constraint/solve_constraints.js +147 -33
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +1750 -1747
- package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
- package/src/engine/physics/gjk/gjk_epa_penetration.d.ts +12 -8
- package/src/engine/physics/gjk/gjk_epa_penetration.d.ts.map +1 -1
- package/src/engine/physics/gjk/gjk_epa_penetration.js +447 -158
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/convex_convex_manifold.js +22 -25
- package/src/engine/physics/narrowphase/convex_decomposition.d.ts +32 -13
- package/src/engine/physics/narrowphase/convex_decomposition.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/convex_decomposition.js +61 -65
- package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/mesh_convex_hull.js +13 -8
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_concave.js +5 -3
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_hit.js +81 -78
- package/src/engine/sound/SoundEngine.d.ts.map +1 -1
- package/src/engine/sound/SoundEngine.js +28 -0
- package/src/engine/sound/dB2Volume.d.ts +1 -1
- package/src/engine/sound/dB2Volume.d.ts.map +1 -1
- package/src/engine/sound/dB2Volume.js +1 -1
- package/src/engine/sound/ecs/SoundController.d.ts +4 -0
- package/src/engine/sound/ecs/SoundController.d.ts.map +1 -1
- package/src/engine/sound/ecs/SoundController.js +4 -0
- package/src/engine/sound/ecs/SoundControllerSystem.d.ts +5 -0
- package/src/engine/sound/ecs/SoundControllerSystem.d.ts.map +1 -1
- package/src/engine/sound/ecs/SoundControllerSystem.js +5 -0
- package/src/engine/sound/ecs/audio/AudioEmitter.d.ts +69 -0
- package/src/engine/sound/ecs/audio/AudioEmitter.d.ts.map +1 -0
- package/src/engine/sound/ecs/audio/AudioEmitter.js +83 -0
- package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts +97 -0
- package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts.map +1 -0
- package/src/engine/sound/ecs/audio/AudioEmitterSystem.js +238 -0
- package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts +90 -0
- package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts.map +1 -0
- package/src/engine/sound/ecs/audio/LiveEmitterSet.js +324 -0
- package/src/engine/sound/ecs/audio/SpatialAudioIndex.d.ts +59 -0
- package/src/engine/sound/ecs/audio/SpatialAudioIndex.d.ts.map +1 -0
- package/src/engine/sound/ecs/audio/SpatialAudioIndex.js +140 -0
- package/src/engine/sound/ecs/emitter/SoundEmitter.d.ts +16 -65
- package/src/engine/sound/ecs/emitter/SoundEmitter.d.ts.map +1 -1
- package/src/engine/sound/ecs/emitter/SoundEmitter.js +19 -224
- package/src/engine/sound/ecs/emitter/SoundEmitterComponentContext.d.ts +26 -29
- package/src/engine/sound/ecs/emitter/SoundEmitterComponentContext.d.ts.map +1 -1
- package/src/engine/sound/ecs/emitter/SoundEmitterComponentContext.js +168 -135
- package/src/engine/sound/ecs/emitter/SoundEmitterSystem.d.ts +36 -59
- package/src/engine/sound/ecs/emitter/SoundEmitterSystem.d.ts.map +1 -1
- package/src/engine/sound/ecs/emitter/SoundEmitterSystem.js +154 -390
- package/src/engine/sound/ecs/emitter/SoundTrack.d.ts +20 -23
- package/src/engine/sound/ecs/emitter/SoundTrack.d.ts.map +1 -1
- package/src/engine/sound/ecs/emitter/SoundTrack.js +34 -152
- package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +993 -0
- package/src/engine/sound/sopra/README.md +643 -7
- package/src/engine/sound/sopra/SopraEngine.d.ts +229 -0
- package/src/engine/sound/sopra/SopraEngine.d.ts.map +1 -0
- package/src/engine/sound/sopra/SopraEngine.js +423 -0
- package/src/engine/sound/sopra/asset/AssetManagerBufferProvider.d.ts +26 -0
- package/src/engine/sound/sopra/asset/AssetManagerBufferProvider.d.ts.map +1 -0
- package/src/engine/sound/sopra/asset/AssetManagerBufferProvider.js +71 -0
- package/src/engine/sound/sopra/asset/BufferProvider.d.ts +24 -0
- package/src/engine/sound/sopra/asset/BufferProvider.d.ts.map +1 -0
- package/src/engine/sound/sopra/asset/BufferProvider.js +29 -0
- package/src/engine/sound/sopra/asset/StubBufferProvider.d.ts +31 -0
- package/src/engine/sound/sopra/asset/StubBufferProvider.d.ts.map +1 -0
- package/src/engine/sound/sopra/asset/StubBufferProvider.js +58 -0
- package/src/engine/sound/sopra/definition/BusDefinition.d.ts +83 -0
- package/src/engine/sound/sopra/definition/BusDefinition.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/BusDefinition.js +142 -0
- package/src/engine/sound/sopra/definition/BusDefinitionSerializationAdapter.d.ts +17 -0
- package/src/engine/sound/sopra/definition/BusDefinitionSerializationAdapter.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/BusDefinitionSerializationAdapter.js +54 -0
- package/src/engine/sound/sopra/definition/DuckingRule.d.ts +71 -0
- package/src/engine/sound/sopra/definition/DuckingRule.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/DuckingRule.js +106 -0
- package/src/engine/sound/sopra/definition/DuckingRuleSerializationAdapter.d.ts +18 -0
- package/src/engine/sound/sopra/definition/DuckingRuleSerializationAdapter.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/DuckingRuleSerializationAdapter.js +31 -0
- package/src/engine/sound/sopra/definition/EventDescription.d.ts +132 -0
- package/src/engine/sound/sopra/definition/EventDescription.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/EventDescription.js +259 -0
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts +17 -0
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.js +71 -0
- package/src/engine/sound/sopra/definition/MixerSnapshot.d.ts +51 -0
- package/src/engine/sound/sopra/definition/MixerSnapshot.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/MixerSnapshot.js +83 -0
- package/src/engine/sound/sopra/definition/MixerSnapshotSerializationAdapter.d.ts +18 -0
- package/src/engine/sound/sopra/definition/MixerSnapshotSerializationAdapter.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/MixerSnapshotSerializationAdapter.js +39 -0
- package/src/engine/sound/sopra/definition/ParameterDefinition.d.ts +72 -0
- package/src/engine/sound/sopra/definition/ParameterDefinition.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/ParameterDefinition.js +117 -0
- package/src/engine/sound/sopra/definition/ParameterDefinitionSerializationAdapter.d.ts +18 -0
- package/src/engine/sound/sopra/definition/ParameterDefinitionSerializationAdapter.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/ParameterDefinitionSerializationAdapter.js +31 -0
- package/src/engine/sound/sopra/definition/SopraPanningModel.d.ts +14 -0
- package/src/engine/sound/sopra/definition/SopraPanningModel.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/SopraPanningModel.js +20 -0
- package/src/engine/sound/sopra/definition/VoiceStealMode.d.ts +10 -0
- package/src/engine/sound/sopra/definition/VoiceStealMode.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/VoiceStealMode.js +18 -0
- package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.d.ts +93 -0
- package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.js +109 -0
- package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts +80 -0
- package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.js +181 -0
- package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClipSerializationAdapter.d.ts +17 -0
- package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClipSerializationAdapter.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClipSerializationAdapter.js +74 -0
- package/src/engine/sound/sopra/definition/clip/ContainerAudioClip.d.ts +34 -0
- package/src/engine/sound/sopra/definition/clip/ContainerAudioClip.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/clip/ContainerAudioClip.js +100 -0
- package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts +101 -0
- package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.js +230 -0
- package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClipSerializationAdapter.d.ts +17 -0
- package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClipSerializationAdapter.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClipSerializationAdapter.js +54 -0
- package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts +103 -0
- package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/clip/SampleAudioClip.js +191 -0
- package/src/engine/sound/sopra/definition/clip/SampleAudioClipSerializationAdapter.d.ts +18 -0
- package/src/engine/sound/sopra/definition/clip/SampleAudioClipSerializationAdapter.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/clip/SampleAudioClipSerializationAdapter.js +39 -0
- package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts +40 -0
- package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.js +91 -0
- package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClipSerializationAdapter.d.ts +17 -0
- package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClipSerializationAdapter.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClipSerializationAdapter.js +42 -0
- package/src/engine/sound/sopra/definition/clip/SilenceAudioClip.d.ts +44 -0
- package/src/engine/sound/sopra/definition/clip/SilenceAudioClip.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/clip/SilenceAudioClip.js +77 -0
- package/src/engine/sound/sopra/definition/clip/SilenceAudioClipSerializationAdapter.d.ts +18 -0
- package/src/engine/sound/sopra/definition/clip/SilenceAudioClipSerializationAdapter.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/clip/SilenceAudioClipSerializationAdapter.js +27 -0
- package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts +65 -0
- package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.js +131 -0
- package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClipSerializationAdapter.d.ts +17 -0
- package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClipSerializationAdapter.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClipSerializationAdapter.js +41 -0
- package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts +24 -0
- package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.js +24 -0
- package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts +70 -0
- package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/effect/CompressorEffect.js +120 -0
- package/src/engine/sound/sopra/definition/effect/CompressorEffectSerializationAdapter.d.ts +18 -0
- package/src/engine/sound/sopra/definition/effect/CompressorEffectSerializationAdapter.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/effect/CompressorEffectSerializationAdapter.js +31 -0
- package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts +74 -0
- package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/effect/EqEffect.js +128 -0
- package/src/engine/sound/sopra/definition/effect/EqEffectSerializationAdapter.d.ts +18 -0
- package/src/engine/sound/sopra/definition/effect/EqEffectSerializationAdapter.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/effect/EqEffectSerializationAdapter.js +29 -0
- package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts +49 -0
- package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/effect/ReverbEffect.js +101 -0
- package/src/engine/sound/sopra/definition/effect/ReverbEffectSerializationAdapter.d.ts +18 -0
- package/src/engine/sound/sopra/definition/effect/ReverbEffectSerializationAdapter.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/effect/ReverbEffectSerializationAdapter.js +25 -0
- package/src/engine/sound/sopra/legacy/soundEmitterToEventDescription.d.ts +31 -0
- package/src/engine/sound/sopra/legacy/soundEmitterToEventDescription.d.ts.map +1 -0
- package/src/engine/sound/sopra/legacy/soundEmitterToEventDescription.js +106 -0
- package/src/engine/sound/sopra/runtime/BusGraph.d.ts +79 -0
- package/src/engine/sound/sopra/runtime/BusGraph.d.ts.map +1 -0
- package/src/engine/sound/sopra/runtime/BusGraph.js +227 -0
- package/src/engine/sound/sopra/runtime/EventInstance.d.ts +144 -0
- package/src/engine/sound/sopra/runtime/EventInstance.d.ts.map +1 -0
- package/src/engine/sound/sopra/runtime/EventInstance.js +579 -0
- package/src/engine/sound/sopra/runtime/ParameterStore.d.ts +42 -0
- package/src/engine/sound/sopra/runtime/ParameterStore.d.ts.map +1 -0
- package/src/engine/sound/sopra/runtime/ParameterStore.js +98 -0
- package/src/engine/sound/sopra/runtime/SopraPlaybackContext.d.ts +42 -0
- package/src/engine/sound/sopra/runtime/SopraPlaybackContext.d.ts.map +1 -0
- package/src/engine/sound/sopra/runtime/SopraPlaybackContext.js +68 -0
- package/src/engine/sound/sopra/runtime/Voice.d.ts +67 -0
- package/src/engine/sound/sopra/runtime/Voice.d.ts.map +1 -0
- package/src/engine/sound/sopra/runtime/Voice.js +145 -0
- package/src/engine/sound/sopra/runtime/VoiceManager.d.ts +38 -0
- package/src/engine/sound/sopra/runtime/VoiceManager.d.ts.map +1 -0
- package/src/engine/sound/sopra/runtime/VoiceManager.js +136 -0
- package/src/engine/sound/sopra/runtime/VoicePool.d.ts +12 -0
- package/src/engine/sound/sopra/runtime/VoicePool.d.ts.map +1 -0
- package/src/engine/sound/sopra/runtime/VoicePool.js +17 -0
- package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts +11 -0
- package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts.map +1 -0
- package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.js +42 -0
- package/src/engine/sound/sopra/serialization/sopraJSON.d.ts +33 -0
- package/src/engine/sound/sopra/serialization/sopraJSON.d.ts.map +1 -0
- package/src/engine/sound/sopra/serialization/sopraJSON.js +99 -0
- package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts +27 -0
- package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts.map +1 -0
- package/src/engine/sound/sopra/serialization/sopraSerializationHarness.js +49 -0
- package/src/engine/sound/sopra/util/MockAudioContext.d.ts +74 -0
- package/src/engine/sound/sopra/util/MockAudioContext.d.ts.map +1 -0
- package/src/engine/sound/sopra/util/MockAudioContext.js +215 -0
- package/src/engine/sound/sopra/util/buildAttenuationCurve.d.ts +15 -0
- package/src/engine/sound/sopra/util/buildAttenuationCurve.d.ts.map +1 -0
- package/src/engine/sound/sopra/util/buildAttenuationCurve.js +40 -0
- package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts +34 -0
- package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts.map +1 -0
- package/src/engine/sound/sopra/util/fadeOutAndStop.js +60 -0
- package/src/engine/sound/volume2dB.d.ts +1 -1
- package/src/engine/sound/volume2dB.d.ts.map +1 -1
- package/src/engine/sound/volume2dB.js +1 -1
- package/src/engine/graphics/sh3/path_tracer/sampling/v3_orthonormal_matrix_from_normal.d.ts.map +0 -1
- package/src/engine/physics/narrowphase/ray_shapes.d.ts +0 -66
- package/src/engine/physics/narrowphase/ray_shapes.d.ts.map +0 -1
- package/src/engine/physics/narrowphase/ray_shapes.js +0 -187
- package/src/engine/sound/ecs/emitter/SoundEmitterChannel.d.ts +0 -23
- package/src/engine/sound/ecs/emitter/SoundEmitterChannel.d.ts.map +0 -1
- package/src/engine/sound/ecs/emitter/SoundEmitterChannel.js +0 -32
- package/src/engine/sound/ecs/emitter/SoundTrackNodes.d.ts +0 -18
- package/src/engine/sound/ecs/emitter/SoundTrackNodes.d.ts.map +0 -1
- package/src/engine/sound/ecs/emitter/SoundTrackNodes.js +0 -18
- package/src/engine/sound/sopra/AbstractAudioClip.d.ts +0 -26
- package/src/engine/sound/sopra/AbstractAudioClip.d.ts.map +0 -1
- package/src/engine/sound/sopra/AbstractAudioClip.js +0 -29
- package/src/engine/sound/sopra/ContainerAudioClip.d.ts +0 -12
- package/src/engine/sound/sopra/ContainerAudioClip.d.ts.map +0 -1
- package/src/engine/sound/sopra/ContainerAudioClip.js +0 -13
- package/src/engine/sound/sopra/RandomContainerAudioClip.d.ts +0 -12
- package/src/engine/sound/sopra/RandomContainerAudioClip.d.ts.map +0 -1
- package/src/engine/sound/sopra/RandomContainerAudioClip.js +0 -15
- package/src/engine/sound/sopra/SequenceContainerAudioClip.d.ts +0 -7
- package/src/engine/sound/sopra/SequenceContainerAudioClip.d.ts.map +0 -1
- package/src/engine/sound/sopra/SequenceContainerAudioClip.js +0 -8
- package/src/engine/sound/sopra/SilenceAudioClip.d.ts +0 -13
- package/src/engine/sound/sopra/SilenceAudioClip.d.ts.map +0 -1
- package/src/engine/sound/sopra/SilenceAudioClip.js +0 -15
- /package/src/{engine/graphics/sh3/path_tracer/sampling → core/geom/vec3}/v3_orthonormal_matrix_from_normal.d.ts +0 -0
|
@@ -1,7 +1,643 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
# Sopra
|
|
2
|
+
|
|
3
|
+
An object-oriented, data-driven sound engine for the meep ECS, loosely modelled on FMOD / Wwise
|
|
4
|
+
concepts (events, a mixer bus tree, RTPC parameters, voice stealing, snapshots, ducking) but built
|
|
5
|
+
around meep's own data model instead of an opaque middleware bank.
|
|
6
|
+
|
|
7
|
+
Sopra is **ECS-agnostic at its core**. The engine (`SopraEngine` + its services) knows nothing about
|
|
8
|
+
entities; it is a self-contained audio renderer driven by a single clock. A thin ECS layer
|
|
9
|
+
(`AudioEmitter` + `AudioEmitterSystem`, outside this folder under `sound/ecs/audio/`) binds it to the
|
|
10
|
+
game. You can use the core standalone (tools, previews, tests) with nothing but a `BaseAudioContext`
|
|
11
|
+
and a `BufferProvider`.
|
|
12
|
+
|
|
13
|
+
> The name "sopra" is internal. The player-facing / integration layer uses familiar terms
|
|
14
|
+
> (`AudioEmitter`, `AudioEventTrigger`); "sopra" never appears in user-facing API.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## The one idea you must understand: definition vs. runtime
|
|
19
|
+
|
|
20
|
+
Everything in Sopra is split into two halves. Get this and the rest follows.
|
|
21
|
+
|
|
22
|
+
| | **Definition** (authored data) | **Runtime** (transient playback) |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| What | `EventDescription`, the `*AudioClip` graph, `BusDefinition`, `*AudioEffect`, `ParameterDefinition`, `MixerSnapshot`, `DuckingRule` | `EventInstance`, `Voice`, the live WebAudio node graph |
|
|
25
|
+
| Lifetime | Immutable, shared, **serializable** (save/load, network) | Created on trigger, pooled, **never serialized** |
|
|
26
|
+
| Holds | Spec only — urls, gains in dB, curves, child clips, routing | WebAudio nodes, the resolved timeline, the playhead, RNG state |
|
|
27
|
+
| Lives in | `definition/` | `runtime/` |
|
|
28
|
+
|
|
29
|
+
A definition is a recipe. Playing it spawns a transient `EventInstance` that reads the recipe once,
|
|
30
|
+
resolves a concrete timeline of leaf plays, and drives WebAudio nodes. The same definition can back
|
|
31
|
+
many simultaneous instances. **Definitions never hold playback state** — that is the rule the whole
|
|
32
|
+
architecture defends.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
EventDescription (definition)
|
|
36
|
+
│ engine.playEvent(...)
|
|
37
|
+
▼
|
|
38
|
+
EventInstance (runtime) ──spawns──► Voice ──► bus chain ──► destination
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Directory map
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
sopra/
|
|
47
|
+
SopraEngine.js the facade: owns the bus graph, parameters, active instances, the clock
|
|
48
|
+
definition/ immutable, serializable authored data (+ co-located *SerializationAdapter.js)
|
|
49
|
+
EventDescription.js the triggerable unit (root clip + routing + 3D + voice config)
|
|
50
|
+
BusDefinition.js one node in the mixer tree (gain, effect chain, aux sends)
|
|
51
|
+
ParameterDefinition.js a declared RTPC parameter
|
|
52
|
+
MixerSnapshot.js a named set of per-bus target gains
|
|
53
|
+
DuckingRule.js an emulated sidechain duck (trigger bus → target bus)
|
|
54
|
+
SopraPanningModel.js HRTF | EqualPower
|
|
55
|
+
VoiceStealMode.js None | Oldest | Quietest
|
|
56
|
+
clip/ the clip graph — what actually makes sound
|
|
57
|
+
AbstractAudioClip.js base; planTimeline() + loops() + collectSampleClips()
|
|
58
|
+
SampleAudioClip.js a single audio asset (the only buffer-referencing leaf)
|
|
59
|
+
SilenceAudioClip.js dead air (a gap in a sequence)
|
|
60
|
+
SequenceContainerAudioClip.js plays children in order
|
|
61
|
+
RandomContainerAudioClip.js plays one child at weighted random (avoid-repeat)
|
|
62
|
+
SwitchContainerAudioClip.js plays one child chosen by a parameter (discrete)
|
|
63
|
+
BlendContainerAudioClip.js plays several children, gains driven by a parameter (continuous)
|
|
64
|
+
effect/ bus insert effects
|
|
65
|
+
AbstractAudioEffect.js base; build(ctx) → { input, output }
|
|
66
|
+
EqEffect.js BiquadFilter
|
|
67
|
+
CompressorEffect.js DynamicsCompressor
|
|
68
|
+
ReverbEffect.js Convolver (procedural impulse response)
|
|
69
|
+
runtime/ transient playback state
|
|
70
|
+
EventInstance.js one triggered playing of an event (cursor-based scheduler)
|
|
71
|
+
Voice.js / VoicePool.js one pooled AudioBufferSourceNode + its gain/detune
|
|
72
|
+
BusGraph.js instantiates BusDefinitions into chained WebAudio nodes
|
|
73
|
+
VoiceManager.js per-event instance limits + voice stealing
|
|
74
|
+
ParameterStore.js live RTPC values + change bindings
|
|
75
|
+
SopraPlaybackContext.js per-source seeded RNG + random-container history (determinism)
|
|
76
|
+
asset/ how the engine gets decoded buffers
|
|
77
|
+
BufferProvider.js the interface (tryGet sync / get async)
|
|
78
|
+
AssetManagerBufferProvider.js production: pulls from the meep AssetManager
|
|
79
|
+
StubBufferProvider.js in-memory, for tests / offline
|
|
80
|
+
serialization/ sopraJSON (type-tag JSON dispatch) + the binary registry + test harness
|
|
81
|
+
util/ buildAttenuationCurve, fadeOutAndStop, MockAudioContext
|
|
82
|
+
legacy/ SoundEmitter → sopra translator (strangler migration only)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The ECS binding lives **outside** this folder, under `sound/ecs/audio/`: `AudioEmitter.js` (component),
|
|
86
|
+
`AudioEmitterSystem.js` (system), and the spatial-scaling layer `SpatialAudioIndex.js` (BVH cull) +
|
|
87
|
+
`LiveEmitterSet.js` (live/dormant budget) that lets it carry ~100k 3D emitters (see ECS integration).
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Quick start (standalone)
|
|
92
|
+
|
|
93
|
+
The core needs three things: an audio context, a destination node, and a `BufferProvider`.
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
import { SopraEngine } from "./SopraEngine.js";
|
|
97
|
+
import { BufferProvider } from "./asset/BufferProvider.js";
|
|
98
|
+
import { EventDescription } from "./definition/EventDescription.js";
|
|
99
|
+
import { SampleAudioClip } from "./definition/clip/SampleAudioClip.js";
|
|
100
|
+
|
|
101
|
+
// 1. Tell Sopra how to obtain decoded AudioBuffers. Here, a trivial in-memory provider.
|
|
102
|
+
class MapBufferProvider extends BufferProvider {
|
|
103
|
+
constructor(map) { super(); this.map = map; } // Map<url, AudioBuffer>
|
|
104
|
+
tryGet(url, usingAlias) { return this.map.get(url) ?? null; } // sync fast path (already decoded)
|
|
105
|
+
get(url, usingAlias) { // async path (rejects if unavailable)
|
|
106
|
+
const b = this.map.get(url);
|
|
107
|
+
return b ? Promise.resolve(b) : Promise.reject(new Error(`missing ${url}`));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const ctx = new AudioContext();
|
|
112
|
+
const buffers = new Map([["click.ogg", await ctx.decodeAudioData(bytes)]]);
|
|
113
|
+
const engine = new SopraEngine(ctx, ctx.destination, new MapBufferProvider(buffers));
|
|
114
|
+
|
|
115
|
+
// 2. Describe an event (a definition). Reuse it as many times as you like.
|
|
116
|
+
const click = EventDescription.from("ui.click", SampleAudioClip.from("click.ogg"), { busId: "effects" });
|
|
117
|
+
|
|
118
|
+
// 3. Play it. playOneShot auto-releases when the sound finishes.
|
|
119
|
+
engine.playOneShot(click);
|
|
120
|
+
|
|
121
|
+
// 4. Drive the engine once per frame — this is mandatory (see "The update loop").
|
|
122
|
+
function frame() { engine.update(); requestAnimationFrame(frame); }
|
|
123
|
+
frame();
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`SopraEngine` ships with a default bus tree — `master` → (`effects`, `music`, `ambient`) — at the
|
|
127
|
+
legacy mix, so `busId: "effects"` works out of the box.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## The clip graph
|
|
132
|
+
|
|
133
|
+
`EventDescription.rootClip` is a tree of `AbstractAudioClip` nodes. Each node implements
|
|
134
|
+
`planTimeline()`, which the instance calls **once** at trigger time to flatten the tree into a list of
|
|
135
|
+
timed leaf plays. Gain (dB) and pitch (cents) accumulate additively down the tree and are resolved
|
|
136
|
+
there — the runtime never sees an "inherit" sentinel.
|
|
137
|
+
|
|
138
|
+
Only `SampleAudioClip` references a buffer; everything else arranges, selects, or layers.
|
|
139
|
+
|
|
140
|
+
### SampleAudioClip — a single asset (leaf)
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
SampleAudioClip.from("explosion.ogg", {
|
|
144
|
+
gain: -3, // dB, added to the event/parent gain
|
|
145
|
+
pitch: 0, // cents
|
|
146
|
+
loop: false,
|
|
147
|
+
loopStart: 0, loopEnd: 0,
|
|
148
|
+
pitchRandom: 50, // ± cents, randomised per trigger (deterministic, see below)
|
|
149
|
+
gainRandom: 2, // ± dB, randomised per trigger
|
|
150
|
+
usingAlias: false // if true, `url` is an AssetManager alias, not a path
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
A `loop: true` sample plays forever — it makes its event a **persistent source** (see one-shot
|
|
155
|
+
vs. persistent below).
|
|
156
|
+
|
|
157
|
+
### SilenceAudioClip — a gap
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
SilenceAudioClip.from(0.5); // 0.5s of dead air, e.g. between sequence steps
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### SequenceContainerAudioClip — children in order
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
SequenceContainerAudioClip.from([
|
|
167
|
+
SampleAudioClip.from("intro.ogg"),
|
|
168
|
+
SilenceAudioClip.from(0.25),
|
|
169
|
+
SampleAudioClip.from("loop.ogg", { loop: true }) // a looping child is terminal: the sequence
|
|
170
|
+
]); // cannot advance past it
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### RandomContainerAudioClip — one child at weighted random
|
|
174
|
+
|
|
175
|
+
Picks one child per trigger, avoiding the last N picks. Weights are **per-child, one entry per child**
|
|
176
|
+
(default `1` = uniform); a weight of `0` means "never picked".
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
RandomContainerAudioClip.from(
|
|
180
|
+
[SampleAudioClip.from("step1.ogg"), SampleAudioClip.from("step2.ogg"), SampleAudioClip.from("step3.ogg")],
|
|
181
|
+
{ avoidRepeatingLast: 1, weights: [3, 1, 1] } // step1 ~3× as likely; never the same twice in a row
|
|
182
|
+
);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Selection is **deterministic** given a seed (see Determinism) — critical for networked play.
|
|
186
|
+
|
|
187
|
+
### SwitchContainerAudioClip — one child by a parameter (discrete)
|
|
188
|
+
|
|
189
|
+
Reads a parameter once at trigger, rounds + clamps it to a child index. Classic surface-switch:
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
SwitchContainerAudioClip.from(
|
|
193
|
+
[grassClip, stoneClip, woodClip],
|
|
194
|
+
{ parameter: "surface", defaultValue: 0 } // surface 0→grass, 1→stone, 2→wood
|
|
195
|
+
);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### BlendContainerAudioClip — several children, gains by a parameter (continuous)
|
|
199
|
+
|
|
200
|
+
Reads a parameter once at trigger and plays every child whose blend gain is > 0, each scaled by its
|
|
201
|
+
per-child `AnimationCurve` (parameter value → linear gain). A child with no curve is always full.
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
import { AnimationCurve } from "../../animation/curve/AnimationCurve.js";
|
|
205
|
+
|
|
206
|
+
BlendContainerAudioClip.from(
|
|
207
|
+
[calmLayer, stormLayer],
|
|
208
|
+
{
|
|
209
|
+
parameter: "weather",
|
|
210
|
+
blends: [
|
|
211
|
+
AnimationCurve.linear(0, 1, 1, 0), // calm: full at 0, silent at 1
|
|
212
|
+
AnimationCurve.linear(0, 0, 1, 1) // storm: silent at 0, full at 1
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
> v1 Blend is a **trigger-time snapshot**: the mix is fixed when the event starts; it does not
|
|
219
|
+
> re-blend live as the parameter sweeps afterwards. Live per-voice re-blend is a planned follow-up.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Events
|
|
224
|
+
|
|
225
|
+
`EventDescription` is the triggerable unit. It wraps a root clip with routing, 3D, and voice config.
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
import { SopraPanningModel } from "./definition/SopraPanningModel.js";
|
|
229
|
+
import { VoiceStealMode } from "./definition/VoiceStealMode.js";
|
|
230
|
+
|
|
231
|
+
EventDescription.from("monster.roar", rootClip, {
|
|
232
|
+
busId: "effects", // which mixer bus this routes to
|
|
233
|
+
gainDb: 0, // event master gain
|
|
234
|
+
is3D: true, // enable spatialization
|
|
235
|
+
panningModel: SopraPanningModel.HRTF,
|
|
236
|
+
distanceMin: 2, // panner reference distance
|
|
237
|
+
distanceMax: 60, // beyond this the source is culled (virtualized)
|
|
238
|
+
attenuation: AnimationCurve.linear(0, 1, 60, 0), // distance → linear gain multiplier
|
|
239
|
+
maxInstances: 8, // concurrency cap for this event
|
|
240
|
+
priority: 0,
|
|
241
|
+
stealMode: VoiceStealMode.Oldest,
|
|
242
|
+
virtualThresholdDb: -60 // below this post-attenuation gain a voice goes virtual
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Non-3D events ignore the 3D fields; `attenuation` defaults to a flat-1 curve (no attenuation).
|
|
247
|
+
|
|
248
|
+
An `EventDescription` is a plain object — hold it wherever you like (a module constant, a component
|
|
249
|
+
field, a map of your own) and pass it straight to the playback methods:
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
engine.playOneShot(roar, { position: monsterPos });
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Playback and instance control
|
|
258
|
+
|
|
259
|
+
`playEvent` / `playOneShot` / `createInstance` / `crossfade` all take an `EventDescription` directly —
|
|
260
|
+
there is no id registry. Keep a reference to the description and pass it in.
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
// fire-and-forget; auto-releases when the content finishes
|
|
264
|
+
engine.playOneShot(click);
|
|
265
|
+
|
|
266
|
+
// keep the handle to control it; persistent until you stop it
|
|
267
|
+
const inst = engine.playEvent(music, { busId: "music" });
|
|
268
|
+
|
|
269
|
+
// build without starting (e.g. to configure first), then start manually
|
|
270
|
+
const i = engine.createInstance(music);
|
|
271
|
+
i.start(engine.currentTime);
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`playEvent` returns the `EventInstance`, or **`null`** if the play was denied by the voice limit
|
|
275
|
+
(steal mode `None`). Always handle the null.
|
|
276
|
+
|
|
277
|
+
### One-shot vs. persistent
|
|
278
|
+
|
|
279
|
+
- `oneShot: true` (what `playOneShot` sets) — the instance **self-releases** when its timeline
|
|
280
|
+
finishes, and is killed by a `maxLifetime` backstop (default 60s) as a safety net.
|
|
281
|
+
- `oneShot: false` (default for `playEvent`) — the instance **persists** until you stop it. Use this
|
|
282
|
+
for looping music/ambience (whose root clip loops). A *non-looping*, non-one-shot instance plays
|
|
283
|
+
once then sits idle (silent) until stopped — usually not what you want, so loop the clip or use
|
|
284
|
+
`playOneShot`.
|
|
285
|
+
|
|
286
|
+
The ECS `AudioEmitterSystem` derives `oneShot` automatically from `event.rootClip.loops()`.
|
|
287
|
+
|
|
288
|
+
### EventInstance API
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
inst.setGainDb(-6); // immediate volume (dB), composes with 3D attenuation
|
|
292
|
+
inst.fadeToGainDb(0, 2); // click-safe ramp to 0 dB over 2s (does not stop)
|
|
293
|
+
inst.fadeOutAndStop(1.5); // fade to silence over 1.5s, then stop (audio-clock deadline)
|
|
294
|
+
inst.seek(10); // jump the playhead to 10s (keeps the resolved timeline + RNG)
|
|
295
|
+
inst.restart(); // seek(0)
|
|
296
|
+
inst.stop(); // stop now, tear down nodes, fire onEnded
|
|
297
|
+
|
|
298
|
+
inst.playhead; // current position in seconds
|
|
299
|
+
inst.gainDb; // current instance gain (dB)
|
|
300
|
+
inst.state; // EventInstanceState: Initial | Playing | Stopped
|
|
301
|
+
inst.currentGain; // current post-attenuation linear gain
|
|
302
|
+
inst.onEnded.add(i => { ... }); // fires once when the instance fully stops
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Crossfade
|
|
306
|
+
|
|
307
|
+
```js
|
|
308
|
+
// fade `current` out and a fresh play of `calmTrack` in, over 3s
|
|
309
|
+
const next = engine.crossfade(current, calmTrack, { busId: "music", duration: 3 });
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Buses, effects, and sends
|
|
315
|
+
|
|
316
|
+
The mixer is a tree of `BusDefinition`s. Each bus is `input → effect[0] → … → effect[n] → gain`, and
|
|
317
|
+
its output routes into its parent's input (or the engine destination for a root bus, `parentId: ""`).
|
|
318
|
+
|
|
319
|
+
`setBuses` **replaces** the whole tree (and rebuilds the WebAudio graph). Effects and sends are wired
|
|
320
|
+
at build time; there is no live effect insertion.
|
|
321
|
+
|
|
322
|
+
```js
|
|
323
|
+
import { BusDefinition } from "./definition/BusDefinition.js";
|
|
324
|
+
import { EqEffect, EqFilterType } from "./definition/effect/EqEffect.js";
|
|
325
|
+
import { CompressorEffect } from "./definition/effect/CompressorEffect.js";
|
|
326
|
+
import { ReverbEffect } from "./definition/effect/ReverbEffect.js";
|
|
327
|
+
|
|
328
|
+
engine.setBuses([
|
|
329
|
+
BusDefinition.from("master", { gainDb: 0 }), // root (parentId "")
|
|
330
|
+
BusDefinition.from("music", { parentId: "master", gainDb: -6,
|
|
331
|
+
effects: [CompressorEffect.from({ threshold: -18, ratio: 4 })] }),
|
|
332
|
+
BusDefinition.from("sfx", { parentId: "master",
|
|
333
|
+
effects: [EqEffect.from({ filterType: EqFilterType.Highshelf, frequency: 6000, gainDb: -3 })],
|
|
334
|
+
sends: [{ targetBusId: "reverb", levelDb: -9 }] }), // post-fader aux send
|
|
335
|
+
BusDefinition.from("reverb", { parentId: "master", // a "reverb bus": others send to it
|
|
336
|
+
effects: [ReverbEffect.from({ decaySeconds: 2.0, decayPower: 2 })] })
|
|
337
|
+
]);
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
A **send** routes a post-fader copy of a bus into another bus at a given level — the standard way to
|
|
341
|
+
share one reverb across many sources. `ReverbEffect` generates its impulse response procedurally
|
|
342
|
+
(decaying noise), so there is no IR asset to load and `build` stays synchronous.
|
|
343
|
+
|
|
344
|
+
Live bus volume (linear gain):
|
|
345
|
+
|
|
346
|
+
```js
|
|
347
|
+
engine.getBusVolume("music"); // → linear gain
|
|
348
|
+
engine.setBusVolume("music", 0.5);
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Parameters (RTPC)
|
|
354
|
+
|
|
355
|
+
Named float values that drive selection (`Switch`/`Blend`) and automation. They live in the
|
|
356
|
+
`ParameterStore`; `Switch`/`Blend` read them **once at trigger time**.
|
|
357
|
+
|
|
358
|
+
```js
|
|
359
|
+
engine.defineParameter("surface", 0); // declare with a default (no-op if already defined)
|
|
360
|
+
engine.setParameter("surface", 1); // 1 → the next footstep Switch picks child 1 (stone)
|
|
361
|
+
engine.getParameter("surface"); // → 1
|
|
362
|
+
|
|
363
|
+
// Drive a bus volume from a parameter through a curve — updates live as the parameter changes.
|
|
364
|
+
engine.bindParameterToBusVolume("tension", "music", AnimationCurve.linear(0, 0.2, 1, 1));
|
|
365
|
+
engine.setParameter("tension", 0.8); // music bus volume follows the curve immediately
|
|
366
|
+
|
|
367
|
+
// Or bind an arbitrary callback (runs on change, and once now if already set).
|
|
368
|
+
engine.bindParameter("tension", value => console.log("tension is", value));
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Note the timing difference: bus-volume bindings update **live**; clip selection (`Switch`/`Blend`) is
|
|
372
|
+
sampled **when the event is triggered**, so changing a parameter affects the *next* play, not voices
|
|
373
|
+
already sounding.
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## 3D / spatialization
|
|
378
|
+
|
|
379
|
+
Set the listener position, give 3D events a position, and call `update()` each frame to re-attenuate.
|
|
380
|
+
|
|
381
|
+
```js
|
|
382
|
+
engine.setListener(camera.position); // a shared Vector3
|
|
383
|
+
|
|
384
|
+
const inst = engine.playOneShot(roar, { position: monster.position });
|
|
385
|
+
// `position` is held by reference — moving the Vector3 moves the source; update() re-evaluates.
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Per instance, distance attenuation is a custom gain node driven by `attenuation.evaluate(distance)`;
|
|
389
|
+
the `PannerNode` does direction only (rolloff disabled). Beyond `distanceMax`, or below
|
|
390
|
+
`virtualThresholdDb`, the instance goes **virtual**: its voices stop while the playhead keeps
|
|
391
|
+
advancing, and revive at the correct child + buffer offset when it becomes audible again.
|
|
392
|
+
|
|
393
|
+
Virtualization bounds the *voice* (source-node) count among the **live** instances. The bigger lever
|
|
394
|
+
for huge worlds is one tier up, in the ECS layer: the `LiveEmitterSet` keeps all-but-a-budget of the
|
|
395
|
+
registered 3D emitters **dormant** (not even an `EventInstance`), so a scene can register ~100k
|
|
396
|
+
emitters and still tick only ~budget of them. See **ECS integration**.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Voice limits and stealing
|
|
401
|
+
|
|
402
|
+
`VoiceManager` caps concurrent instances **per event** at `EventDescription.maxInstances` and steals
|
|
403
|
+
when full, per the event's `stealMode`:
|
|
404
|
+
|
|
405
|
+
- `None` — over the limit, the new play is dropped (`playEvent` returns `null`).
|
|
406
|
+
- `Oldest` — stop the earliest-started instance.
|
|
407
|
+
- `Quietest` — stop the instance with the lowest post-attenuation gain.
|
|
408
|
+
|
|
409
|
+
Defaults (`maxInstances: 32`, `stealMode: Oldest`) effectively never engage until content opts into a
|
|
410
|
+
low limit — e.g. a machine-gun: `maxInstances: 6`.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Mixer snapshots and ducking
|
|
415
|
+
|
|
416
|
+
### Snapshots — shift the whole mix between game states
|
|
417
|
+
|
|
418
|
+
A `MixerSnapshot` is a named set of per-bus target gains (dB). Apply it instantly or as a click-safe
|
|
419
|
+
ramp.
|
|
420
|
+
|
|
421
|
+
```js
|
|
422
|
+
import { MixerSnapshot } from "./definition/MixerSnapshot.js";
|
|
423
|
+
|
|
424
|
+
const combat = MixerSnapshot.from("combat", [
|
|
425
|
+
{ busId: "music", gainDb: 0 },
|
|
426
|
+
{ busId: "ambient", gainDb: -12 }
|
|
427
|
+
]);
|
|
428
|
+
|
|
429
|
+
engine.applySnapshot(combat, { duration: 1.5 }); // ramp into the combat mix over 1.5s
|
|
430
|
+
const saved = engine.captureSnapshot("pre-combat", ["music", "ambient"]); // grab the current mix to restore later
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Ducking — emulated sidechain
|
|
434
|
+
|
|
435
|
+
WebAudio has no native sidechain, so this is **play-state** ducking: while any instance is live on the
|
|
436
|
+
trigger bus, the target bus is attenuated by `duckDb` (smooth `setTargetAtTime` attack/release),
|
|
437
|
+
restored to its captured nominal when the trigger goes quiet.
|
|
438
|
+
|
|
439
|
+
```js
|
|
440
|
+
import { DuckingRule } from "./definition/DuckingRule.js";
|
|
441
|
+
|
|
442
|
+
// duck music by 8 dB whenever anything plays on the effects bus
|
|
443
|
+
engine.addDucker(DuckingRule.from({
|
|
444
|
+
triggerBusId: "effects", targetBusId: "music",
|
|
445
|
+
duckDb: -8, attack: 0.1, release: 0.4
|
|
446
|
+
}));
|
|
447
|
+
|
|
448
|
+
engine.clearDuckers(); // remove all rules (restoring any currently-ducked target)
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
The trigger matches by `instance.busId`. Duckers are evaluated every `update()`.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Determinism
|
|
456
|
+
|
|
457
|
+
Random selection (`RandomContainer`, per-trigger pitch/gain randomisation) runs on a **seeded** RNG
|
|
458
|
+
held in a `SopraPlaybackContext`. Same seed → identical pick stream → networked clients stay in sync.
|
|
459
|
+
|
|
460
|
+
```js
|
|
461
|
+
// seed from a replicated source, e.g. entity id + fixed-step tick
|
|
462
|
+
engine.playEvent(footsteps, { seed: entityId * 31 + tick });
|
|
463
|
+
|
|
464
|
+
// or supply your own context (its history persists across re-triggers — reuse one per emitter)
|
|
465
|
+
import { SopraPlaybackContext } from "./runtime/SopraPlaybackContext.js";
|
|
466
|
+
const ctx = new SopraPlaybackContext(seed);
|
|
467
|
+
engine.playEvent(footsteps, { playbackContext: ctx });
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
If you supply neither, the engine's shared `defaultPlaybackContext` is used.
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## The update loop
|
|
475
|
+
|
|
476
|
+
`SopraEngine` is driven by a single clock — the injected `AudioContext.currentTime`. **You must call
|
|
477
|
+
`engine.update()` once per frame.** It advances every instance (scheduling/virtualization, 3D
|
|
478
|
+
attenuation, fade-out deadlines, one-shot release) and evaluates ducking rules.
|
|
479
|
+
|
|
480
|
+
```js
|
|
481
|
+
engine.update(); // uses the live clock
|
|
482
|
+
engine.update(someTime); // or pass an explicit context time (tests do this)
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
Nothing self-schedules with `setTimeout`; fades and stops are resolved against the audio clock inside
|
|
486
|
+
`update`, so a late frame never desynchronises audio.
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## Serialization
|
|
491
|
+
|
|
492
|
+
Definitions are fully serializable, two ways:
|
|
493
|
+
|
|
494
|
+
**JSON** — every definition has `toJSON()` / `fromJSON()`. Polymorphic clip/effect trees go through
|
|
495
|
+
type-tagged dispatch in `serialization/sopraJSON.js`:
|
|
496
|
+
|
|
497
|
+
```js
|
|
498
|
+
const json = description.toJSON();
|
|
499
|
+
const restored = new EventDescription();
|
|
500
|
+
restored.fromJSON(json);
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**Binary** — each class has a co-located `<Class>SerializationAdapter.js`. Register them all on a
|
|
504
|
+
`BinarySerializationRegistry` with `populateSopraSerializationRegistry`:
|
|
505
|
+
|
|
506
|
+
```js
|
|
507
|
+
import { BinarySerializationRegistry } from "../../ecs/storage/binary/BinarySerializationRegistry.js";
|
|
508
|
+
import { populateSopraSerializationRegistry } from "./serialization/populateSopraSerializationRegistry.js";
|
|
509
|
+
|
|
510
|
+
const registry = new BinarySerializationRegistry();
|
|
511
|
+
populateSopraSerializationRegistry(registry);
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Numeric fields are stored as float32. `serialization/sopraSerializationHarness.js` provides
|
|
515
|
+
`makeSopraObjectAdapter()` + `binaryRoundTrip()` for round-trip tests.
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## ECS integration
|
|
520
|
+
|
|
521
|
+
In-game you almost never touch `SopraEngine` directly — you add an `AudioEmitter` component.
|
|
522
|
+
|
|
523
|
+
```js
|
|
524
|
+
import { AudioEmitter } from "../ecs/audio/AudioEmitter.js";
|
|
525
|
+
|
|
526
|
+
const emitter = new AudioEmitter();
|
|
527
|
+
emitter.event = EventDescription.from(
|
|
528
|
+
"torch.loop", SampleAudioClip.from("torch.ogg", { loop: true }),
|
|
529
|
+
{ busId: "ambient", is3D: true, distanceMax: 20 }
|
|
530
|
+
);
|
|
531
|
+
emitter.autoplay = true; // this is a looping 3D event -> spatially managed (sounds when the
|
|
532
|
+
// listener is in range + within budget); see the two play paths below
|
|
533
|
+
emitter.volume.set(0.8); // live multiplier on top of the event gain (Vector1)
|
|
534
|
+
|
|
535
|
+
entity.add(emitter).add(new Transform(...));
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
`AudioEmitter` **owns a full `EventDescription`** — the whole clip graph, routing, 3D, voice config —
|
|
539
|
+
as first-class ECS data. There is no global "bank"; the `EntityComponentDataset` *is* the data store.
|
|
540
|
+
|
|
541
|
+
`AudioEmitterSystem(assetManager, soundEngine, liveEmitterSetOptions?)` drives all emitters. It shares
|
|
542
|
+
the one `SopraEngine` (via `SoundEngine.createSopra(bufferProvider)`, idempotent), feeds the
|
|
543
|
+
`SoundListener` pose, and ticks the engine each frame. *How* an emitter plays is decided **once at
|
|
544
|
+
link**, from `autoplay` + the event:
|
|
545
|
+
|
|
546
|
+
- **Spatially managed** — `autoplay && is3D && looping`. Registered with a `LiveEmitterSet` (a BVH
|
|
547
|
+
broadphase, `SpatialAudioIndex`, + a live/dormant lifecycle) and left **dormant**: no instance, no
|
|
548
|
+
nodes, no per-frame work — just one BVH leaf. Each frame the system culls by the listener position and
|
|
549
|
+
promotes the nearest in-range emitters up to a global voice **budget** (default 64), demoting the rest
|
|
550
|
+
(a hard cut when culled out of range, a click-safe fade on contention). This is what lets the engine
|
|
551
|
+
carry **~100,000 registered emitters** while only the audible ~budget ever hold WebAudio nodes — the
|
|
552
|
+
per-frame cost tracks the budget, not the registered count.
|
|
553
|
+
- **Direct** — any other `autoplay` event (2D sounds like music / ambience beds, or finite 3D
|
|
554
|
+
one-shots): played immediately on link, stopped on unlink. Nothing to cull by distance, and these are
|
|
555
|
+
few.
|
|
556
|
+
- **Inert** — `autoplay === false`: neither played nor registered.
|
|
557
|
+
|
|
558
|
+
The entity `Transform.position` is held by the instance (a 3D source tracks the moving entity);
|
|
559
|
+
`AudioEmitter.volume` is a live multiplier carried **through** promote/demote, so a managed emitter
|
|
560
|
+
keeps its volume across a cull. `system.instanceFor(entity)` returns the active instance (or `null`
|
|
561
|
+
while dormant).
|
|
562
|
+
|
|
563
|
+
> **Content rule for managed crowds:** the live `budget` and an event's `maxInstances` are independent
|
|
564
|
+
> caps. If many emitters share one *content-equal* `EventDescription` (e.g. 100k identical birds), give
|
|
565
|
+
> that event `maxInstances ≥ budget` — content-equal events share one polyphony bucket, so a lower cap
|
|
566
|
+
> would gate the budget and fewer than `budget` would actually sound.
|
|
567
|
+
|
|
568
|
+
In production the provider is an `AssetManagerBufferProvider` over the meep `AssetManager` (shared
|
|
569
|
+
decode cache + the alias system).
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## Testing
|
|
574
|
+
|
|
575
|
+
The entire core is unit-testable without real audio: a `MockAudioContext` records the constructed
|
|
576
|
+
graph and scheduled events, and a `StubBufferProvider` hands over fake buffers. Tests assert the graph
|
|
577
|
+
shape, timeline math, voice lifecycle, deterministic selection, and serialization — never sound.
|
|
578
|
+
|
|
579
|
+
```js
|
|
580
|
+
import { SopraEngine } from "./SopraEngine.js";
|
|
581
|
+
import { MockAudioContext } from "./util/MockAudioContext.js";
|
|
582
|
+
import { StubBufferProvider } from "./asset/StubBufferProvider.js";
|
|
583
|
+
|
|
584
|
+
const ctx = new MockAudioContext();
|
|
585
|
+
const provider = new StubBufferProvider();
|
|
586
|
+
provider.set("x.ogg", { duration: 2 }); // a fake buffer is anything with a `duration`
|
|
587
|
+
// provider.failOn("y.ogg"); // simulate a failed/missing asset
|
|
588
|
+
|
|
589
|
+
const engine = new SopraEngine(ctx, ctx.destination, provider);
|
|
590
|
+
const inst = engine.playOneShot(EventDescription.from("e", SampleAudioClip.from("x.ogg")));
|
|
591
|
+
|
|
592
|
+
ctx.currentTime = 2;
|
|
593
|
+
engine.update(2);
|
|
594
|
+
// inst.state === EventInstanceState.Stopped (the one-shot self-released at the end)
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Run the suite from the repo root:
|
|
598
|
+
|
|
599
|
+
```
|
|
600
|
+
npx jest --config jest.conf.json engine/sound
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
## Design rules (don't break these)
|
|
606
|
+
|
|
607
|
+
- **Definitions are immutable, shared, and serializable.** No WebAudio nodes, no playback state, no
|
|
608
|
+
per-instance mutation on a definition. Runtime state lives on `EventInstance` / the
|
|
609
|
+
`SopraPlaybackContext`.
|
|
610
|
+
- **Gain is composed exactly once.** Authored gains are dB and accumulate down the clip tree at plan
|
|
611
|
+
time; bus/instance gains are linear on their own nodes. Don't double-apply.
|
|
612
|
+
- **One clock.** Everything reads `AudioContext.currentTime`; no `setTimeout`-driven audio.
|
|
613
|
+
- **The core never creates an `AudioContext`** and never imports ECS. It takes a context, a
|
|
614
|
+
destination, and a `BufferProvider`.
|
|
615
|
+
- **Errors surface.** Expected failures (missing asset, unresolved alias, limit reached) return a
|
|
616
|
+
sentinel (`null`) or reject; unexpected errors throw. Nothing is silently swallowed.
|
|
617
|
+
- **Determinism is sacred.** Random selection is seeded from a replicated source; never reach for
|
|
618
|
+
`Math.random()` in the playback path.
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
## Glossary
|
|
623
|
+
|
|
624
|
+
Acronyms and shorthand used in this document (and the code).
|
|
625
|
+
|
|
626
|
+
| Term | Expansion | Meaning in Sopra |
|
|
627
|
+
|---|---|---|
|
|
628
|
+
| **2D / 3D** | two- / three-dimensional | A 2D event is non-positional (plays straight into its bus); a 3D event (`is3D: true`) is spatialised — panned and distance-attenuated relative to the listener. |
|
|
629
|
+
| **API** | Application Programming Interface | The public methods/classes you call. |
|
|
630
|
+
| **aux send** | auxiliary send | A post-fader copy of a bus routed into another bus at a level (`BusDefinition.sends`), e.g. feeding a shared reverb bus. |
|
|
631
|
+
| **cents** | — | Pitch unit; 100 cents = 1 semitone, 1200 = 1 octave. All pitch offsets/randomisation are in cents. |
|
|
632
|
+
| **dB** | decibel | Logarithmic gain unit; 0 dB = unity, −6 dB ≈ half power. **All authored gains are in dB**; node gains on the WebAudio graph are linear (the two are converted by `dB2Volume` / `volume2dB`). |
|
|
633
|
+
| **ECS** | Entity Component System | meep's architecture: entities are ids, data lives in components, behaviour in systems. Sopra's *core* is ECS-agnostic; the ECS binding is `AudioEmitter` + `AudioEmitterSystem`. |
|
|
634
|
+
| **EQ** | equaliser | A filter insert (`EqEffect`), backed by a WebAudio `BiquadFilterNode`. |
|
|
635
|
+
| **FMOD / Wwise** | (product names) | Industry audio middleware. Sopra borrows their concepts — events, a bus tree, RTPC, voice stealing, snapshots, ducking — but is built on meep's data model, not a bank file. |
|
|
636
|
+
| **HRTF** | Head-Related Transfer Function | The high-quality (per-source convolution) 3D panning model; one of the `SopraPanningModel` options (the other, `EqualPower`, is cheaper). |
|
|
637
|
+
| **Hz** | hertz | Frequency unit (cycles/second), e.g. an EQ cutoff/centre frequency. |
|
|
638
|
+
| **IR** | Impulse Response | The convolution kernel that defines a reverb's space. `ReverbEffect` **generates one procedurally** (decaying noise), so there is no IR asset to load. |
|
|
639
|
+
| **JSON** | JavaScript Object Notation | One of the two serialization formats (human-readable; via `toJSON`/`fromJSON` + the type-tag dispatch in `sopraJSON.js`). The other is binary. |
|
|
640
|
+
| **Q** | quality factor | A filter's bandwidth/resonance control (`EqEffect.Q`). |
|
|
641
|
+
| **RNG** | Random Number Generator | Sopra's RNG is **seeded** (per `SopraPlaybackContext`) so random selection is reproducible across networked clients. |
|
|
642
|
+
| **RTPC** | Real-Time Parameter Control | A named float "parameter" (FMOD's term). Drives `Switch`/`Blend` selection and bus-volume automation. Stored in the `ParameterStore`; see `defineParameter` / `setParameter`. |
|
|
643
|
+
| **WebAudio** | Web Audio API | The browser audio API Sopra renders through (`AudioContext`, `GainNode`, `PannerNode`, `BiquadFilterNode`, `ConvolverNode`, …). Sopra never creates the `AudioContext` — meep's `SoundEngine` does. |
|