@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.
Files changed (487) hide show
  1. package/README.md +1 -1
  2. package/build/bundle-worker-image-decoder.js +1 -1
  3. package/build/bundle-worker-terrain.js +1 -1
  4. package/editor/view/ecs/ComponentControlView.d.ts +0 -9
  5. package/editor/view/ecs/ComponentControlView.js +2 -98
  6. package/package.json +1 -1
  7. package/src/core/binary/32BitEncoder.js +1 -1
  8. package/src/core/binary/to_half_float_uint16.js +3 -3
  9. package/src/core/bvh2/bvh3/ebvh_build_hierarchy_radix.d.ts.map +1 -1
  10. package/src/core/bvh2/bvh3/ebvh_build_hierarchy_radix.js +275 -253
  11. package/src/core/cache/Cache.d.ts.map +1 -1
  12. package/src/core/cache/Cache.js +7 -0
  13. package/src/core/cache/FrequencySketch.d.ts.map +1 -1
  14. package/src/core/cache/FrequencySketch.js +8 -4
  15. package/src/core/clipboard/obtainClipBoard.d.ts +6 -0
  16. package/src/core/clipboard/obtainClipBoard.d.ts.map +1 -0
  17. package/src/core/clipboard/obtainClipBoard.js +29 -0
  18. package/src/core/clipboard/safeClipboardReadText.d.ts +6 -0
  19. package/src/core/clipboard/safeClipboardReadText.d.ts.map +1 -0
  20. package/src/core/clipboard/safeClipboardReadText.js +55 -0
  21. package/src/core/clipboard/safeClipboardWriteText.d.ts +8 -0
  22. package/src/core/clipboard/safeClipboardWriteText.d.ts.map +1 -0
  23. package/src/core/clipboard/safeClipboardWriteText.js +23 -0
  24. package/src/core/collection/array/array_quick_sort_by_lookup_map.js +1 -1
  25. package/src/core/collection/array/array_set_diff_sorting.d.ts.map +1 -1
  26. package/src/core/collection/array/array_set_diff_sorting.js +4 -1
  27. package/src/core/collection/array/array_shuffle.d.ts.map +1 -1
  28. package/src/core/collection/array/array_shuffle.js +30 -27
  29. package/src/core/collection/array/binarySearchLowIndex.d.ts.map +1 -1
  30. package/src/core/collection/array/binarySearchLowIndex.js +4 -3
  31. package/src/core/collection/array/typed/array_buffer_hash.js +1 -1
  32. package/src/core/collection/array/typed/is_typed_array_equals.d.ts.map +1 -1
  33. package/src/core/collection/array/typed/is_typed_array_equals.js +12 -2
  34. package/src/core/collection/heap/BinaryHeap.d.ts.map +1 -1
  35. package/src/core/collection/heap/BinaryHeap.js +12 -2
  36. package/src/core/collection/queue/Deque.d.ts.map +1 -1
  37. package/src/core/collection/queue/Deque.js +10 -8
  38. package/src/core/collection/table/RowFirstTable.d.ts.map +1 -1
  39. package/src/core/collection/table/RowFirstTable.js +4 -2
  40. package/src/core/collection/table/RowFirstTableSpec.js +2 -2
  41. package/src/core/color/operations/color_lerp.d.ts.map +1 -1
  42. package/src/core/color/operations/color_lerp.js +10 -3
  43. package/src/core/color/rgb2uint32.js +1 -1
  44. package/src/core/color/rgbe9995_to_rgb.js +1 -1
  45. package/src/core/function/objectsEqual.d.ts.map +1 -1
  46. package/src/core/function/objectsEqual.js +2 -1
  47. package/src/core/geom/2d/aabb/AABB2.d.ts.map +1 -1
  48. package/src/core/geom/2d/aabb/AABB2.js +12 -11
  49. package/src/core/geom/2d/convex-hull/convex_hull_jarvis_2d.d.ts.map +1 -1
  50. package/src/core/geom/2d/convex-hull/convex_hull_jarvis_2d.js +30 -4
  51. package/src/core/geom/2d/convex-hull/fixed_convex_hull_relaxation.d.ts.map +1 -1
  52. package/src/core/geom/2d/convex-hull/fixed_convex_hull_relaxation.js +6 -2
  53. package/src/core/geom/2d/hash-grid/SpatialHashGrid.d.ts.map +1 -1
  54. package/src/core/geom/2d/hash-grid/SpatialHashGrid.js +388 -386
  55. package/src/core/geom/2d/hash-grid/shg_query_elements_line.d.ts.map +1 -1
  56. package/src/core/geom/2d/hash-grid/shg_query_elements_line.js +8 -3
  57. package/src/core/geom/2d/quad-tree/QuadTreeDatum.d.ts.map +1 -1
  58. package/src/core/geom/2d/quad-tree/QuadTreeDatum.js +9 -1
  59. package/src/core/geom/2d/quad-tree/qt_query_data_nearest_to_point.d.ts +3 -1
  60. package/src/core/geom/2d/quad-tree/qt_query_data_nearest_to_point.d.ts.map +1 -1
  61. package/src/core/geom/2d/quad-tree/qt_query_data_nearest_to_point.js +3 -1
  62. package/src/core/geom/2d/quad-tree-binary/QuadTree.js +714 -714
  63. package/src/core/geom/2d/r-tree/StaticR2Tree.d.ts.map +1 -1
  64. package/src/core/geom/2d/r-tree/StaticR2Tree.js +5 -4
  65. package/src/core/geom/3d/aabb/aabb3_detailed_volume_intersection.d.ts.map +1 -1
  66. package/src/core/geom/3d/aabb/aabb3_detailed_volume_intersection.js +33 -29
  67. package/src/core/geom/3d/aabb/aabb3_near_distance_to_intersection_ray_segment.d.ts.map +1 -1
  68. package/src/core/geom/3d/aabb/aabb3_near_distance_to_intersection_ray_segment.js +3 -1
  69. package/src/core/geom/3d/aabb/aabb3_signed_distance_to_aabb3.d.ts.map +1 -1
  70. package/src/core/geom/3d/aabb/aabb3_signed_distance_to_aabb3.js +10 -7
  71. package/src/core/geom/3d/aabb/aabb3_transformed_compute_plane_side.d.ts.map +1 -1
  72. package/src/core/geom/3d/aabb/aabb3_transformed_compute_plane_side.js +30 -9
  73. package/src/core/geom/3d/aabb/compute_aabb_from_points.js +3 -3
  74. package/src/core/geom/3d/box/box3_raycast.d.ts +37 -0
  75. package/src/core/geom/3d/box/box3_raycast.d.ts.map +1 -0
  76. package/src/core/geom/3d/box/box3_raycast.js +81 -0
  77. package/src/core/geom/3d/capsule/capsule_raycast.d.ts +35 -0
  78. package/src/core/geom/3d/capsule/capsule_raycast.d.ts.map +1 -0
  79. package/src/core/geom/3d/capsule/capsule_raycast.js +93 -0
  80. package/src/core/geom/3d/cone/compute_bounding_cone_of_2_cones.d.ts.map +1 -1
  81. package/src/core/geom/3d/cone/compute_bounding_cone_of_2_cones.js +4 -0
  82. package/src/core/geom/3d/frustum/frustum3_computeNearestPointToPoint.js +1 -1
  83. package/src/core/geom/3d/line/line3_compute_segment_point_distance_eikonal.d.ts.map +1 -1
  84. package/src/core/geom/3d/line/line3_compute_segment_point_distance_eikonal.js +3 -2
  85. package/src/core/geom/3d/mat4/decompose_matrix_4_array.d.ts.map +1 -1
  86. package/src/core/geom/3d/mat4/decompose_matrix_4_array.js +12 -2
  87. package/src/core/geom/3d/mat4/eulerAnglesFromMatrix.js +2 -2
  88. package/src/core/geom/3d/mat4/m4_multiply_alphatensor.d.ts +1 -1
  89. package/src/core/geom/3d/mat4/m4_multiply_alphatensor.d.ts.map +1 -1
  90. package/src/core/geom/3d/mat4/m4_multiply_alphatensor.js +19 -13
  91. package/src/core/geom/3d/octahedra/octahedral_direction_to_uv.d.ts.map +1 -1
  92. package/src/core/geom/3d/octahedra/octahedral_direction_to_uv.js +3 -2
  93. package/src/core/geom/3d/plane/plane3_compute_plane_intersection.js +3 -2
  94. package/src/core/geom/3d/shape/MeshShape3D.d.ts.map +1 -1
  95. package/src/core/geom/3d/shape/MeshShape3D.js +7 -0
  96. package/src/core/geom/3d/shape/UnionShape3D.d.ts.map +1 -1
  97. package/src/core/geom/3d/shape/UnionShape3D.js +3 -2
  98. package/src/core/geom/3d/shape/util/shape3d_voxelize_to_grid.d.ts.map +1 -1
  99. package/src/core/geom/3d/shape/util/shape3d_voxelize_to_grid.js +153 -148
  100. package/src/core/geom/3d/sphere/harmonics/sh3_dering_optimize_positive.d.ts.map +1 -1
  101. package/src/core/geom/3d/sphere/harmonics/sh3_dering_optimize_positive.js +7 -0
  102. package/src/core/geom/3d/sphere/harmonics/sh3_sample_by_direction.d.ts.map +1 -1
  103. package/src/core/geom/3d/sphere/harmonics/sh3_sample_by_direction.js +13 -10
  104. package/src/core/geom/3d/sphere/sphere_projected_sphere_radius_sqr.d.ts +1 -1
  105. package/src/core/geom/3d/sphere/sphere_projected_sphere_radius_sqr.js +2 -2
  106. package/src/core/geom/3d/sphere/sphere_raycast.d.ts +33 -0
  107. package/src/core/geom/3d/sphere/sphere_raycast.d.ts.map +1 -0
  108. package/src/core/geom/3d/sphere/sphere_raycast.js +47 -0
  109. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_tet_get_neighbours.d.ts +24 -0
  110. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_tet_get_neighbours.d.ts.map +1 -0
  111. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_tet_get_neighbours.js +39 -0
  112. package/src/core/geom/3d/tetrahedra/triangle/trace_triangular_depth_map.d.ts.map +1 -1
  113. package/src/core/geom/3d/tetrahedra/triangle/trace_triangular_depth_map.js +4 -2
  114. package/src/core/geom/3d/topology/bounds/computeTriangleClusterNormalBoundingCone.js +3 -3
  115. package/src/core/geom/3d/topology/simplify/decimate_edge_collapse_snap.js +1 -1
  116. package/src/core/geom/3d/topology/tm_vertex_compute_normal.d.ts.map +1 -1
  117. package/src/core/geom/3d/topology/tm_vertex_compute_normal.js +4 -2
  118. package/src/core/geom/3d/util/make_justified_point_grid.d.ts.map +1 -1
  119. package/src/core/geom/3d/util/make_justified_point_grid.js +18 -10
  120. package/src/core/geom/ConicRay.d.ts.map +1 -1
  121. package/src/core/geom/ConicRay.js +11 -13
  122. package/src/core/geom/packing/max-rect/removeRedundantBoxes.d.ts.map +1 -1
  123. package/src/core/geom/packing/max-rect/removeRedundantBoxes.js +19 -4
  124. package/src/core/geom/vec3/v3_array_copy.d.ts +3 -3
  125. package/src/core/geom/vec3/v3_array_copy.d.ts.map +1 -1
  126. package/src/core/geom/vec3/v3_array_copy.js +2 -2
  127. package/src/core/geom/vec3/v3_cross.d.ts +17 -0
  128. package/src/core/geom/vec3/v3_cross.d.ts.map +1 -0
  129. package/src/core/geom/vec3/v3_cross.js +20 -0
  130. package/src/core/geom/vec3/v3_orthonormal_matrix_from_normal.d.ts.map +1 -0
  131. package/src/{engine/graphics/sh3/path_tracer/sampling → core/geom/vec3}/v3_orthonormal_matrix_from_normal.js +1 -1
  132. package/src/core/geom/vec3/v3_subtract.d.ts +16 -0
  133. package/src/core/geom/vec3/v3_subtract.d.ts.map +1 -0
  134. package/src/core/geom/vec3/v3_subtract.js +19 -0
  135. package/src/core/graph/coloring/colorizeGraph.js +2 -2
  136. package/src/core/graph/csr/CSRGraph.d.ts.map +1 -1
  137. package/src/core/graph/csr/CSRGraph.js +325 -319
  138. package/src/core/graph/layout/CircleLayout.d.ts.map +1 -1
  139. package/src/core/graph/layout/CircleLayout.js +8 -6
  140. package/src/core/graph/metis/native/refine/compute_kway_params.d.ts.map +1 -1
  141. package/src/core/graph/metis/native/refine/compute_kway_params.js +139 -138
  142. package/src/core/graph/mn_graph_coarsen.d.ts.map +1 -1
  143. package/src/core/graph/mn_graph_coarsen.js +4 -2
  144. package/src/core/graph/v2/NodeContainer.js +7 -7
  145. package/src/core/localization/LocalizationEngine.js +1 -1
  146. package/src/core/math/bell_membership_function.d.ts.map +1 -1
  147. package/src/core/math/bell_membership_function.js +3 -1
  148. package/src/core/math/complex/complex_add.d.ts +4 -4
  149. package/src/core/math/complex/complex_add.d.ts.map +1 -1
  150. package/src/core/math/complex/complex_add.js +3 -3
  151. package/src/core/math/complex/complex_div.d.ts +4 -4
  152. package/src/core/math/complex/complex_div.d.ts.map +1 -1
  153. package/src/core/math/complex/complex_div.js +3 -3
  154. package/src/core/math/complex/complex_mul.d.ts +4 -4
  155. package/src/core/math/complex/complex_mul.d.ts.map +1 -1
  156. package/src/core/math/complex/complex_mul.js +3 -3
  157. package/src/core/math/complex/complex_sub.d.ts +4 -4
  158. package/src/core/math/complex/complex_sub.d.ts.map +1 -1
  159. package/src/core/math/complex/complex_sub.js +3 -3
  160. package/src/core/math/idct_1d.d.ts +4 -4
  161. package/src/core/math/idct_1d.d.ts.map +1 -1
  162. package/src/core/math/idct_1d.js +3 -3
  163. package/src/core/math/noise/create_simplex_noise_2d.d.ts.map +1 -1
  164. package/src/core/math/noise/create_simplex_noise_2d.js +4 -2
  165. package/src/core/math/noise/sdnoise.d.ts.map +1 -1
  166. package/src/core/math/noise/sdnoise.js +12 -9
  167. package/src/core/math/physics/mie/compute_bhmie_optical_properties.d.ts.map +1 -1
  168. package/src/core/math/physics/mie/compute_bhmie_optical_properties.js +94 -50
  169. package/src/core/math/physics/mie/lorenz_mie_coefs.d.ts +3 -6
  170. package/src/core/math/physics/mie/lorenz_mie_coefs.d.ts.map +1 -1
  171. package/src/core/math/physics/mie/lorenz_mie_coefs.js +180 -157
  172. package/src/core/math/physics/mie/mie_ab_to_optical_properties.d.ts +3 -4
  173. package/src/core/math/physics/mie/mie_ab_to_optical_properties.d.ts.map +1 -1
  174. package/src/core/math/physics/mie/mie_ab_to_optical_properties.js +47 -21
  175. package/src/core/math/random/randomIntegerBetween.d.ts.map +1 -1
  176. package/src/core/math/random/randomIntegerBetween.js +4 -1
  177. package/src/core/math/solveCubic.d.ts.map +1 -1
  178. package/src/core/math/solveCubic.js +95 -82
  179. package/src/core/math/spline/computeCatmullRomSplineUniformDistance.d.ts.map +1 -1
  180. package/src/core/math/spline/computeCatmullRomSplineUniformDistance.js +13 -0
  181. package/src/core/math/statistics/softmax.js +1 -1
  182. package/src/core/model/node-graph/visual/NodeGraphVisualData.d.ts +1 -0
  183. package/src/core/model/node-graph/visual/NodeGraphVisualData.d.ts.map +1 -1
  184. package/src/core/model/node-graph/visual/NodeGraphVisualData.js +2 -1
  185. package/src/core/model/node-graph/visual/NodeVisualData.js +1 -1
  186. package/src/core/model/object/ImmutableObjectPool.d.ts +7 -0
  187. package/src/core/model/object/ImmutableObjectPool.d.ts.map +1 -1
  188. package/src/core/model/object/ImmutableObjectPool.js +20 -10
  189. package/src/core/model/reactive/evaluation/MultiPredicateEvaluator.d.ts.map +1 -1
  190. package/src/core/model/reactive/evaluation/MultiPredicateEvaluator.js +39 -2
  191. package/src/core/model/reactive/model/terminal/ReactiveReference.d.ts.map +1 -1
  192. package/src/core/model/reactive/model/terminal/ReactiveReference.js +2 -0
  193. package/src/core/parser/simple/readHexToken.d.ts.map +1 -1
  194. package/src/core/parser/simple/readHexToken.js +6 -0
  195. package/src/core/primitives/numbers/number_pretty_print.d.ts.map +1 -1
  196. package/src/core/primitives/numbers/number_pretty_print.js +4 -1
  197. package/src/core/primitives/strings/string_jaro_winkler.js +1 -1
  198. package/src/core/process/CompositeProcess.js +1 -1
  199. package/src/core/process/action/AsynchronousDelayAction.d.ts.map +1 -1
  200. package/src/core/process/action/AsynchronousDelayAction.js +3 -0
  201. package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
  202. package/src/core/process/executor/ConcurrentExecutor.js +3 -2
  203. package/src/core/process/task/util/randomCountTask.d.ts.map +1 -1
  204. package/src/core/process/task/util/randomCountTask.js +3 -1
  205. package/src/core/process/undo/ActionProcessor.d.ts.map +1 -1
  206. package/src/core/process/undo/ActionProcessor.js +5 -3
  207. package/src/core/process/worker/WorkerBuilder.js +3 -3
  208. package/src/engine/animation/curve/AnimationCurve.d.ts.map +1 -1
  209. package/src/engine/animation/curve/AnimationCurve.js +4 -2
  210. package/src/engine/control/first-person/DESIGN.md +1 -1
  211. package/src/engine/control/first-person/FirstPersonMotionPhase.d.ts +55 -0
  212. package/src/engine/control/first-person/FirstPersonMotionPhase.d.ts.map +1 -0
  213. package/src/engine/control/first-person/FirstPersonMotionPhase.js +134 -0
  214. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +23 -2
  215. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  216. package/src/engine/control/first-person/FirstPersonPlayerController.js +1 -1
  217. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +168 -0
  218. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  219. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +115 -0
  220. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +71 -0
  221. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  222. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +255 -55
  223. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts +82 -43
  224. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts.map +1 -1
  225. package/src/engine/control/first-person/abilities/LedgeGrab.js +405 -213
  226. package/src/engine/control/first-person/abilities/Mantle.d.ts +6 -0
  227. package/src/engine/control/first-person/abilities/Mantle.d.ts.map +1 -1
  228. package/src/engine/control/first-person/abilities/Mantle.js +104 -45
  229. package/src/engine/control/first-person/abilities/ScrambleUp.d.ts +61 -0
  230. package/src/engine/control/first-person/abilities/ScrambleUp.d.ts.map +1 -0
  231. package/src/engine/control/first-person/abilities/ScrambleUp.js +182 -0
  232. package/src/engine/control/first-person/math/jumpDynamics.d.ts +84 -0
  233. package/src/engine/control/first-person/math/jumpDynamics.d.ts.map +1 -0
  234. package/src/engine/control/first-person/math/jumpDynamics.js +108 -0
  235. package/src/engine/control/first-person/prototype_first_person_controller.js +45 -1
  236. package/src/engine/graphics/camera/testClippingPlaneComputation.js +1 -1
  237. package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.d.ts.map +1 -1
  238. package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +8 -0
  239. package/src/engine/graphics/ecs/path/tube/prototypeAnimatedPathMask.js +1 -1
  240. package/src/engine/graphics/particles/particular/engine/utils/volume/prototypeParticleVolume.js +1 -1
  241. package/src/engine/graphics/render/buffer/buffers/prototypeNormalFrameBuffer.js +1 -1
  242. package/src/engine/graphics/render/forward_plus/plugin/ptototypeFPPlugin.js +1 -1
  243. package/src/engine/graphics/render/visibility/hiz/prototypeHiZ.js +1 -1
  244. package/src/engine/graphics/sh3/path_tracer/texture/sample_material.js +1 -1
  245. package/src/engine/graphics/shadows/testShadowMapRendering.js +1 -1
  246. package/src/engine/physics/CONSTRAINT_SOLVER_BENCH_LOG.md +208 -0
  247. package/src/engine/physics/CONSTRAINT_SOLVER_IMPROVEMENTS_PLAN.md +364 -0
  248. package/src/engine/physics/PLAN.md +6 -5
  249. package/src/engine/physics/constraint/solve_constraints.d.ts +4 -1
  250. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
  251. package/src/engine/physics/constraint/solve_constraints.js +147 -33
  252. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  253. package/src/engine/physics/ecs/PhysicsSystem.js +1750 -1747
  254. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
  255. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts +12 -8
  256. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts.map +1 -1
  257. package/src/engine/physics/gjk/gjk_epa_penetration.js +447 -158
  258. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -1
  259. package/src/engine/physics/narrowphase/convex_convex_manifold.js +22 -25
  260. package/src/engine/physics/narrowphase/convex_decomposition.d.ts +32 -13
  261. package/src/engine/physics/narrowphase/convex_decomposition.d.ts.map +1 -1
  262. package/src/engine/physics/narrowphase/convex_decomposition.js +61 -65
  263. package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts.map +1 -1
  264. package/src/engine/physics/narrowphase/mesh_convex_hull.js +13 -8
  265. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
  266. package/src/engine/physics/narrowphase/refine_ray_concave.js +5 -3
  267. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -1
  268. package/src/engine/physics/narrowphase/refine_ray_hit.js +81 -78
  269. package/src/engine/sound/SoundEngine.d.ts.map +1 -1
  270. package/src/engine/sound/SoundEngine.js +28 -0
  271. package/src/engine/sound/dB2Volume.d.ts +1 -1
  272. package/src/engine/sound/dB2Volume.d.ts.map +1 -1
  273. package/src/engine/sound/dB2Volume.js +1 -1
  274. package/src/engine/sound/ecs/SoundController.d.ts +4 -0
  275. package/src/engine/sound/ecs/SoundController.d.ts.map +1 -1
  276. package/src/engine/sound/ecs/SoundController.js +4 -0
  277. package/src/engine/sound/ecs/SoundControllerSystem.d.ts +5 -0
  278. package/src/engine/sound/ecs/SoundControllerSystem.d.ts.map +1 -1
  279. package/src/engine/sound/ecs/SoundControllerSystem.js +5 -0
  280. package/src/engine/sound/ecs/audio/AudioEmitter.d.ts +69 -0
  281. package/src/engine/sound/ecs/audio/AudioEmitter.d.ts.map +1 -0
  282. package/src/engine/sound/ecs/audio/AudioEmitter.js +83 -0
  283. package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts +97 -0
  284. package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts.map +1 -0
  285. package/src/engine/sound/ecs/audio/AudioEmitterSystem.js +238 -0
  286. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts +90 -0
  287. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts.map +1 -0
  288. package/src/engine/sound/ecs/audio/LiveEmitterSet.js +324 -0
  289. package/src/engine/sound/ecs/audio/SpatialAudioIndex.d.ts +59 -0
  290. package/src/engine/sound/ecs/audio/SpatialAudioIndex.d.ts.map +1 -0
  291. package/src/engine/sound/ecs/audio/SpatialAudioIndex.js +140 -0
  292. package/src/engine/sound/ecs/emitter/SoundEmitter.d.ts +16 -65
  293. package/src/engine/sound/ecs/emitter/SoundEmitter.d.ts.map +1 -1
  294. package/src/engine/sound/ecs/emitter/SoundEmitter.js +19 -224
  295. package/src/engine/sound/ecs/emitter/SoundEmitterComponentContext.d.ts +26 -29
  296. package/src/engine/sound/ecs/emitter/SoundEmitterComponentContext.d.ts.map +1 -1
  297. package/src/engine/sound/ecs/emitter/SoundEmitterComponentContext.js +168 -135
  298. package/src/engine/sound/ecs/emitter/SoundEmitterSystem.d.ts +36 -59
  299. package/src/engine/sound/ecs/emitter/SoundEmitterSystem.d.ts.map +1 -1
  300. package/src/engine/sound/ecs/emitter/SoundEmitterSystem.js +154 -390
  301. package/src/engine/sound/ecs/emitter/SoundTrack.d.ts +20 -23
  302. package/src/engine/sound/ecs/emitter/SoundTrack.d.ts.map +1 -1
  303. package/src/engine/sound/ecs/emitter/SoundTrack.js +34 -152
  304. package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +993 -0
  305. package/src/engine/sound/sopra/README.md +643 -7
  306. package/src/engine/sound/sopra/SopraEngine.d.ts +229 -0
  307. package/src/engine/sound/sopra/SopraEngine.d.ts.map +1 -0
  308. package/src/engine/sound/sopra/SopraEngine.js +423 -0
  309. package/src/engine/sound/sopra/asset/AssetManagerBufferProvider.d.ts +26 -0
  310. package/src/engine/sound/sopra/asset/AssetManagerBufferProvider.d.ts.map +1 -0
  311. package/src/engine/sound/sopra/asset/AssetManagerBufferProvider.js +71 -0
  312. package/src/engine/sound/sopra/asset/BufferProvider.d.ts +24 -0
  313. package/src/engine/sound/sopra/asset/BufferProvider.d.ts.map +1 -0
  314. package/src/engine/sound/sopra/asset/BufferProvider.js +29 -0
  315. package/src/engine/sound/sopra/asset/StubBufferProvider.d.ts +31 -0
  316. package/src/engine/sound/sopra/asset/StubBufferProvider.d.ts.map +1 -0
  317. package/src/engine/sound/sopra/asset/StubBufferProvider.js +58 -0
  318. package/src/engine/sound/sopra/definition/BusDefinition.d.ts +83 -0
  319. package/src/engine/sound/sopra/definition/BusDefinition.d.ts.map +1 -0
  320. package/src/engine/sound/sopra/definition/BusDefinition.js +142 -0
  321. package/src/engine/sound/sopra/definition/BusDefinitionSerializationAdapter.d.ts +17 -0
  322. package/src/engine/sound/sopra/definition/BusDefinitionSerializationAdapter.d.ts.map +1 -0
  323. package/src/engine/sound/sopra/definition/BusDefinitionSerializationAdapter.js +54 -0
  324. package/src/engine/sound/sopra/definition/DuckingRule.d.ts +71 -0
  325. package/src/engine/sound/sopra/definition/DuckingRule.d.ts.map +1 -0
  326. package/src/engine/sound/sopra/definition/DuckingRule.js +106 -0
  327. package/src/engine/sound/sopra/definition/DuckingRuleSerializationAdapter.d.ts +18 -0
  328. package/src/engine/sound/sopra/definition/DuckingRuleSerializationAdapter.d.ts.map +1 -0
  329. package/src/engine/sound/sopra/definition/DuckingRuleSerializationAdapter.js +31 -0
  330. package/src/engine/sound/sopra/definition/EventDescription.d.ts +132 -0
  331. package/src/engine/sound/sopra/definition/EventDescription.d.ts.map +1 -0
  332. package/src/engine/sound/sopra/definition/EventDescription.js +259 -0
  333. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts +17 -0
  334. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts.map +1 -0
  335. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.js +71 -0
  336. package/src/engine/sound/sopra/definition/MixerSnapshot.d.ts +51 -0
  337. package/src/engine/sound/sopra/definition/MixerSnapshot.d.ts.map +1 -0
  338. package/src/engine/sound/sopra/definition/MixerSnapshot.js +83 -0
  339. package/src/engine/sound/sopra/definition/MixerSnapshotSerializationAdapter.d.ts +18 -0
  340. package/src/engine/sound/sopra/definition/MixerSnapshotSerializationAdapter.d.ts.map +1 -0
  341. package/src/engine/sound/sopra/definition/MixerSnapshotSerializationAdapter.js +39 -0
  342. package/src/engine/sound/sopra/definition/ParameterDefinition.d.ts +72 -0
  343. package/src/engine/sound/sopra/definition/ParameterDefinition.d.ts.map +1 -0
  344. package/src/engine/sound/sopra/definition/ParameterDefinition.js +117 -0
  345. package/src/engine/sound/sopra/definition/ParameterDefinitionSerializationAdapter.d.ts +18 -0
  346. package/src/engine/sound/sopra/definition/ParameterDefinitionSerializationAdapter.d.ts.map +1 -0
  347. package/src/engine/sound/sopra/definition/ParameterDefinitionSerializationAdapter.js +31 -0
  348. package/src/engine/sound/sopra/definition/SopraPanningModel.d.ts +14 -0
  349. package/src/engine/sound/sopra/definition/SopraPanningModel.d.ts.map +1 -0
  350. package/src/engine/sound/sopra/definition/SopraPanningModel.js +20 -0
  351. package/src/engine/sound/sopra/definition/VoiceStealMode.d.ts +10 -0
  352. package/src/engine/sound/sopra/definition/VoiceStealMode.d.ts.map +1 -0
  353. package/src/engine/sound/sopra/definition/VoiceStealMode.js +18 -0
  354. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.d.ts +93 -0
  355. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.d.ts.map +1 -0
  356. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.js +109 -0
  357. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts +80 -0
  358. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts.map +1 -0
  359. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.js +181 -0
  360. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClipSerializationAdapter.d.ts +17 -0
  361. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClipSerializationAdapter.d.ts.map +1 -0
  362. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClipSerializationAdapter.js +74 -0
  363. package/src/engine/sound/sopra/definition/clip/ContainerAudioClip.d.ts +34 -0
  364. package/src/engine/sound/sopra/definition/clip/ContainerAudioClip.d.ts.map +1 -0
  365. package/src/engine/sound/sopra/definition/clip/ContainerAudioClip.js +100 -0
  366. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts +101 -0
  367. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts.map +1 -0
  368. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.js +230 -0
  369. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClipSerializationAdapter.d.ts +17 -0
  370. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClipSerializationAdapter.d.ts.map +1 -0
  371. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClipSerializationAdapter.js +54 -0
  372. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts +103 -0
  373. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts.map +1 -0
  374. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.js +191 -0
  375. package/src/engine/sound/sopra/definition/clip/SampleAudioClipSerializationAdapter.d.ts +18 -0
  376. package/src/engine/sound/sopra/definition/clip/SampleAudioClipSerializationAdapter.d.ts.map +1 -0
  377. package/src/engine/sound/sopra/definition/clip/SampleAudioClipSerializationAdapter.js +39 -0
  378. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts +40 -0
  379. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts.map +1 -0
  380. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.js +91 -0
  381. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClipSerializationAdapter.d.ts +17 -0
  382. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClipSerializationAdapter.d.ts.map +1 -0
  383. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClipSerializationAdapter.js +42 -0
  384. package/src/engine/sound/sopra/definition/clip/SilenceAudioClip.d.ts +44 -0
  385. package/src/engine/sound/sopra/definition/clip/SilenceAudioClip.d.ts.map +1 -0
  386. package/src/engine/sound/sopra/definition/clip/SilenceAudioClip.js +77 -0
  387. package/src/engine/sound/sopra/definition/clip/SilenceAudioClipSerializationAdapter.d.ts +18 -0
  388. package/src/engine/sound/sopra/definition/clip/SilenceAudioClipSerializationAdapter.d.ts.map +1 -0
  389. package/src/engine/sound/sopra/definition/clip/SilenceAudioClipSerializationAdapter.js +27 -0
  390. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts +65 -0
  391. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts.map +1 -0
  392. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.js +131 -0
  393. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClipSerializationAdapter.d.ts +17 -0
  394. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClipSerializationAdapter.d.ts.map +1 -0
  395. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClipSerializationAdapter.js +41 -0
  396. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts +24 -0
  397. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts.map +1 -0
  398. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.js +24 -0
  399. package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts +70 -0
  400. package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts.map +1 -0
  401. package/src/engine/sound/sopra/definition/effect/CompressorEffect.js +120 -0
  402. package/src/engine/sound/sopra/definition/effect/CompressorEffectSerializationAdapter.d.ts +18 -0
  403. package/src/engine/sound/sopra/definition/effect/CompressorEffectSerializationAdapter.d.ts.map +1 -0
  404. package/src/engine/sound/sopra/definition/effect/CompressorEffectSerializationAdapter.js +31 -0
  405. package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts +74 -0
  406. package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts.map +1 -0
  407. package/src/engine/sound/sopra/definition/effect/EqEffect.js +128 -0
  408. package/src/engine/sound/sopra/definition/effect/EqEffectSerializationAdapter.d.ts +18 -0
  409. package/src/engine/sound/sopra/definition/effect/EqEffectSerializationAdapter.d.ts.map +1 -0
  410. package/src/engine/sound/sopra/definition/effect/EqEffectSerializationAdapter.js +29 -0
  411. package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts +49 -0
  412. package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts.map +1 -0
  413. package/src/engine/sound/sopra/definition/effect/ReverbEffect.js +101 -0
  414. package/src/engine/sound/sopra/definition/effect/ReverbEffectSerializationAdapter.d.ts +18 -0
  415. package/src/engine/sound/sopra/definition/effect/ReverbEffectSerializationAdapter.d.ts.map +1 -0
  416. package/src/engine/sound/sopra/definition/effect/ReverbEffectSerializationAdapter.js +25 -0
  417. package/src/engine/sound/sopra/legacy/soundEmitterToEventDescription.d.ts +31 -0
  418. package/src/engine/sound/sopra/legacy/soundEmitterToEventDescription.d.ts.map +1 -0
  419. package/src/engine/sound/sopra/legacy/soundEmitterToEventDescription.js +106 -0
  420. package/src/engine/sound/sopra/runtime/BusGraph.d.ts +79 -0
  421. package/src/engine/sound/sopra/runtime/BusGraph.d.ts.map +1 -0
  422. package/src/engine/sound/sopra/runtime/BusGraph.js +227 -0
  423. package/src/engine/sound/sopra/runtime/EventInstance.d.ts +144 -0
  424. package/src/engine/sound/sopra/runtime/EventInstance.d.ts.map +1 -0
  425. package/src/engine/sound/sopra/runtime/EventInstance.js +579 -0
  426. package/src/engine/sound/sopra/runtime/ParameterStore.d.ts +42 -0
  427. package/src/engine/sound/sopra/runtime/ParameterStore.d.ts.map +1 -0
  428. package/src/engine/sound/sopra/runtime/ParameterStore.js +98 -0
  429. package/src/engine/sound/sopra/runtime/SopraPlaybackContext.d.ts +42 -0
  430. package/src/engine/sound/sopra/runtime/SopraPlaybackContext.d.ts.map +1 -0
  431. package/src/engine/sound/sopra/runtime/SopraPlaybackContext.js +68 -0
  432. package/src/engine/sound/sopra/runtime/Voice.d.ts +67 -0
  433. package/src/engine/sound/sopra/runtime/Voice.d.ts.map +1 -0
  434. package/src/engine/sound/sopra/runtime/Voice.js +145 -0
  435. package/src/engine/sound/sopra/runtime/VoiceManager.d.ts +38 -0
  436. package/src/engine/sound/sopra/runtime/VoiceManager.d.ts.map +1 -0
  437. package/src/engine/sound/sopra/runtime/VoiceManager.js +136 -0
  438. package/src/engine/sound/sopra/runtime/VoicePool.d.ts +12 -0
  439. package/src/engine/sound/sopra/runtime/VoicePool.d.ts.map +1 -0
  440. package/src/engine/sound/sopra/runtime/VoicePool.js +17 -0
  441. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts +11 -0
  442. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts.map +1 -0
  443. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.js +42 -0
  444. package/src/engine/sound/sopra/serialization/sopraJSON.d.ts +33 -0
  445. package/src/engine/sound/sopra/serialization/sopraJSON.d.ts.map +1 -0
  446. package/src/engine/sound/sopra/serialization/sopraJSON.js +99 -0
  447. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts +27 -0
  448. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts.map +1 -0
  449. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.js +49 -0
  450. package/src/engine/sound/sopra/util/MockAudioContext.d.ts +74 -0
  451. package/src/engine/sound/sopra/util/MockAudioContext.d.ts.map +1 -0
  452. package/src/engine/sound/sopra/util/MockAudioContext.js +215 -0
  453. package/src/engine/sound/sopra/util/buildAttenuationCurve.d.ts +15 -0
  454. package/src/engine/sound/sopra/util/buildAttenuationCurve.d.ts.map +1 -0
  455. package/src/engine/sound/sopra/util/buildAttenuationCurve.js +40 -0
  456. package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts +34 -0
  457. package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts.map +1 -0
  458. package/src/engine/sound/sopra/util/fadeOutAndStop.js +60 -0
  459. package/src/engine/sound/volume2dB.d.ts +1 -1
  460. package/src/engine/sound/volume2dB.d.ts.map +1 -1
  461. package/src/engine/sound/volume2dB.js +1 -1
  462. package/src/engine/graphics/sh3/path_tracer/sampling/v3_orthonormal_matrix_from_normal.d.ts.map +0 -1
  463. package/src/engine/physics/narrowphase/ray_shapes.d.ts +0 -66
  464. package/src/engine/physics/narrowphase/ray_shapes.d.ts.map +0 -1
  465. package/src/engine/physics/narrowphase/ray_shapes.js +0 -187
  466. package/src/engine/sound/ecs/emitter/SoundEmitterChannel.d.ts +0 -23
  467. package/src/engine/sound/ecs/emitter/SoundEmitterChannel.d.ts.map +0 -1
  468. package/src/engine/sound/ecs/emitter/SoundEmitterChannel.js +0 -32
  469. package/src/engine/sound/ecs/emitter/SoundTrackNodes.d.ts +0 -18
  470. package/src/engine/sound/ecs/emitter/SoundTrackNodes.d.ts.map +0 -1
  471. package/src/engine/sound/ecs/emitter/SoundTrackNodes.js +0 -18
  472. package/src/engine/sound/sopra/AbstractAudioClip.d.ts +0 -26
  473. package/src/engine/sound/sopra/AbstractAudioClip.d.ts.map +0 -1
  474. package/src/engine/sound/sopra/AbstractAudioClip.js +0 -29
  475. package/src/engine/sound/sopra/ContainerAudioClip.d.ts +0 -12
  476. package/src/engine/sound/sopra/ContainerAudioClip.d.ts.map +0 -1
  477. package/src/engine/sound/sopra/ContainerAudioClip.js +0 -13
  478. package/src/engine/sound/sopra/RandomContainerAudioClip.d.ts +0 -12
  479. package/src/engine/sound/sopra/RandomContainerAudioClip.d.ts.map +0 -1
  480. package/src/engine/sound/sopra/RandomContainerAudioClip.js +0 -15
  481. package/src/engine/sound/sopra/SequenceContainerAudioClip.d.ts +0 -7
  482. package/src/engine/sound/sopra/SequenceContainerAudioClip.d.ts.map +0 -1
  483. package/src/engine/sound/sopra/SequenceContainerAudioClip.js +0 -8
  484. package/src/engine/sound/sopra/SilenceAudioClip.d.ts +0 -13
  485. package/src/engine/sound/sopra/SilenceAudioClip.d.ts.map +0 -1
  486. package/src/engine/sound/sopra/SilenceAudioClip.js +0 -15
  487. /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
- ## Sopra
2
-
3
- Object-oriented sound engine
4
-
5
- ---
6
-
7
- Loosely based on concepts from FMOD and Wwise
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. |