@jgengine/shell 0.18.1 → 0.21.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/CHANGELOG.md +504 -7
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +132 -17
- package/dist/Shell3dPresentation.d.ts +5 -11
- package/dist/Shell3dPresentation.js +73 -11
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +20 -2
- package/dist/ShellHudPresentation.js +41 -6
- package/dist/audio/AudioComponents.js +6 -2
- package/dist/audio/audioEngine.d.ts +24 -0
- package/dist/audio/audioEngine.js +231 -83
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +9 -3
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +3 -2
- package/dist/camera/GameFirstPersonCamera.d.ts +6 -4
- package/dist/camera/GameFirstPersonCamera.js +68 -27
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +76 -62
- package/dist/camera/chaseHeading.d.ts +9 -0
- package/dist/camera/chaseHeading.js +10 -0
- package/dist/camera/customMuzzle.d.ts +14 -0
- package/dist/camera/customMuzzle.js +68 -0
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- package/dist/camera/rigResolve.d.ts +3 -0
- package/dist/camera/rigResolve.js +11 -0
- package/dist/combat/ProjectileModels.d.ts +31 -0
- package/dist/combat/ProjectileModels.js +70 -0
- package/dist/commandSink.d.ts +1 -2
- package/dist/commandSink.js +3 -2
- package/dist/defineGame.d.ts +19 -3
- package/dist/defineGame.js +39 -7
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +15 -167
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +16 -2
- package/dist/devtools/devtoolsReports.d.ts +68 -0
- package/dist/devtools/devtoolsReports.js +100 -0
- package/dist/devtools/latencyInstrumentation.d.ts +3 -0
- package/dist/devtools/latencyInstrumentation.js +11 -0
- package/dist/devtools/playtestProbe.d.ts +4 -0
- package/dist/devtools/playtestProbe.js +22 -0
- package/dist/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.js +35 -20
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/DayNightSky.js +5 -5
- package/dist/environment/Daylight.d.ts +35 -1
- package/dist/environment/Daylight.js +98 -28
- package/dist/environment/EnvironmentScene.js +11 -10
- package/dist/environment/VolumetricClouds.js +20 -2
- package/dist/environment/daylightCycle.d.ts +20 -0
- package/dist/environment/daylightCycle.js +59 -7
- package/dist/environment/sunShadowMath.d.ts +45 -0
- package/dist/environment/sunShadowMath.js +50 -0
- package/dist/input/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +11 -1
- package/dist/input/gamepadSource.js +117 -71
- package/dist/input/mouseLook.js +2 -1
- package/dist/input/padHaptics.d.ts +17 -0
- package/dist/input/padHaptics.js +32 -0
- package/dist/input/pointerLock.d.ts +11 -0
- package/dist/input/pointerLock.js +19 -0
- package/dist/inputSink.d.ts +6 -6
- package/dist/inputSink.js +60 -21
- package/dist/lazyShell3dPresentation.d.ts +9 -0
- package/dist/lazyShell3dPresentation.js +24 -0
- package/dist/materialOverride.d.ts +42 -0
- package/dist/materialOverride.js +95 -36
- package/dist/multiplayer.d.ts +33 -3
- package/dist/multiplayer.js +105 -22
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +12 -2
- package/dist/postfx/PostProcessing.d.ts +11 -1
- package/dist/postfx/PostProcessing.js +14 -42
- package/dist/postfx/gradeShader.d.ts +1 -1
- package/dist/postfx/gradeShader.js +30 -2
- package/dist/postfx/shadowPrepass.d.ts +3 -0
- package/dist/postfx/shadowPrepass.js +18 -0
- package/dist/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -0
- package/dist/presentationRecovery.d.ts +75 -0
- package/dist/presentationRecovery.js +204 -0
- package/dist/presentationRecovery3d.d.ts +4 -0
- package/dist/presentationRecovery3d.js +32 -0
- package/dist/presentationResolve.d.ts +8 -1
- package/dist/presentationResolve.js +5 -2
- package/dist/render/CascadedShadows.d.ts +7 -2
- package/dist/render/CascadedShadows.js +98 -20
- package/dist/render/EnvironmentLighting.js +74 -35
- package/dist/render/HairCards.d.ts +7 -0
- package/dist/render/HairCards.js +23 -0
- package/dist/render/SceneModels.js +52 -78
- package/dist/render/SkinnedInstances.d.ts +91 -0
- package/dist/render/SkinnedInstances.js +249 -0
- package/dist/render/TextureFiltering.d.ts +16 -0
- package/dist/render/TextureFiltering.js +64 -0
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -0
- package/dist/render/materialAppearanceSignals.d.ts +23 -0
- package/dist/render/materialAppearanceSignals.js +58 -0
- package/dist/render/materialAsset.d.ts +98 -0
- package/dist/render/materialAsset.js +454 -0
- package/dist/render/measureBounds.js +16 -3
- package/dist/render/measureCollisionMesh.js +8 -1
- package/dist/render/modelAssets.d.ts +8 -0
- package/dist/render/modelAssets.js +43 -0
- package/dist/render/modelBindPose.d.ts +5 -0
- package/dist/render/modelBindPose.js +35 -0
- package/dist/render/modelLoad.d.ts +32 -0
- package/dist/render/modelLoad.js +70 -1
- package/dist/render/modelNodeVisibility.d.ts +6 -0
- package/dist/render/modelNodeVisibility.js +18 -0
- package/dist/render/modelRender.d.ts +18 -1
- package/dist/render/modelRender.js +81 -3
- package/dist/render/projectedOverdraw.d.ts +35 -0
- package/dist/render/projectedOverdraw.js +204 -0
- package/dist/render/resolveModel.d.ts +12 -2
- package/dist/render/resolveModel.js +25 -4
- package/dist/render/rigNode.d.ts +26 -0
- package/dist/render/rigNode.js +32 -0
- package/dist/render/spriteRender.d.ts +4 -0
- package/dist/render/spriteRender.js +5 -0
- package/dist/render/staticShapeInstances.d.ts +34 -0
- package/dist/render/staticShapeInstances.js +78 -0
- package/dist/render/useFootIk.d.ts +53 -4
- package/dist/render/useFootIk.js +262 -70
- package/dist/render/useModelAnimation.d.ts +50 -0
- package/dist/render/useModelAnimation.js +245 -99
- package/dist/render/useModelInstance.d.ts +25 -0
- package/dist/render/useModelInstance.js +43 -0
- package/dist/scatter/scatterModels.d.ts +3 -2
- package/dist/scatter/scatterModels.js +15 -7
- package/dist/scene/AuthoredScene.d.ts +23 -6
- package/dist/scene/AuthoredScene.js +46 -9
- package/dist/scene/CityRenderer.js +6 -6
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- package/dist/settings/appliedSettings.d.ts +9 -3
- package/dist/settings/appliedSettings.js +6 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- package/dist/shellControlSuspension.d.ts +18 -0
- package/dist/shellControlSuspension.js +28 -0
- package/dist/shellInputPublication.d.ts +28 -0
- package/dist/shellInputPublication.js +49 -0
- package/dist/shellPointerInput.d.ts +18 -0
- package/dist/shellPointerInput.js +49 -0
- package/dist/structures/BuildingKitBatch.d.ts +3 -1
- package/dist/structures/BuildingKitBatch.js +35 -56
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +33 -22
- package/dist/structures/buildingKitMaterials.d.ts +20 -0
- package/dist/structures/buildingKitMaterials.js +70 -0
- package/dist/structures/buildingSpatialBatch.d.ts +11 -0
- package/dist/structures/buildingSpatialBatch.js +45 -0
- package/dist/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -0
- package/dist/terrain/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +59 -5
- package/dist/terrain/grassGeometry.d.ts +10 -0
- package/dist/terrain/grassGeometry.js +55 -0
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.d.ts +1 -1
- package/dist/useShellMultiplayerSync.js +9 -3
- package/dist/vfx/ParticleField.d.ts +23 -20
- package/dist/vfx/ParticleField.js +168 -50
- package/dist/visibility/CullingProvider.js +2 -62
- package/dist/visibility/cullingDriver.d.ts +9 -0
- package/dist/visibility/cullingDriver.js +68 -0
- package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
- package/dist/weather/AuthoredWeatherLayer.js +31 -0
- package/dist/weather/DustField.d.ts +2 -1
- package/dist/weather/DustField.js +17 -3
- package/dist/weather/FireSpreadLayer.d.ts +4 -2
- package/dist/weather/FireSpreadLayer.js +5 -11
- package/dist/weather/LightningStrike.d.ts +4 -1
- package/dist/weather/LightningStrike.js +13 -17
- package/dist/weather/RainField.d.ts +2 -1
- package/dist/weather/RainField.js +17 -3
- package/dist/weather/RainImpactField.d.ts +16 -0
- package/dist/weather/RainImpactField.js +89 -0
- package/dist/weather/SnowField.d.ts +2 -1
- package/dist/weather/SnowField.js +17 -3
- package/dist/weather/WeatherLayer.d.ts +11 -2
- package/dist/weather/WeatherLayer.js +30 -6
- package/dist/weather/index.d.ts +2 -0
- package/dist/weather/index.js +2 -0
- package/dist/weather/weatherMath.d.ts +2 -0
- package/dist/weather/weatherMath.js +8 -0
- package/dist/weather/weatherShelter.d.ts +18 -0
- package/dist/weather/weatherShelter.js +75 -0
- package/dist/weather/weatherUniforms.d.ts +40 -1
- package/dist/weather/weatherUniforms.js +29 -9
- package/dist/world/AuthoredFlocks.d.ts +22 -0
- package/dist/world/AuthoredFlocks.js +87 -0
- package/dist/world/StaticObjectModels.d.ts +8 -0
- package/dist/world/StaticObjectModels.js +75 -0
- package/dist/world/WorldHud.d.ts +14 -6
- package/dist/world/WorldHud.js +25 -12
- package/dist/world/WorldParticles.d.ts +10 -23
- package/dist/world/WorldParticles.js +210 -62
- package/dist/world/WorldScene.d.ts +3 -1
- package/dist/world/WorldScene.js +41 -26
- package/dist/world/staticObjectBatches.d.ts +35 -0
- package/dist/world/staticObjectBatches.js +163 -0
- package/dist/world/worldBarSamples.d.ts +37 -33
- package/dist/world/worldBarSamples.js +117 -75
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +28 -6
- package/skills/jgengine/api.md +139 -99
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +10 -2
- package/skills/jgengine-ui/SKILL.md +16 -17
- package/skills/jgengine-ui/api.md +603 -365
- package/skills/jgengine-ui/capabilities.md +131 -1
- package/skills/jgengine-ui/reference-primitives.md +179 -0
- package/skills/jgengine-ui/reference.md +4 -81
- package/skills/jgengine-world/SKILL.md +25 -4
- package/skills/jgengine-world/api.md +1045 -655
- package/skills/jgengine-world/capabilities.md +246 -9
- package/skills/jgengine-world/recipes/character-animation.md +62 -0
- package/skills/jgengine-world/recipes/controls.md +48 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +55 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
- package/skills/jgengine-world/reference.md +180 -4
- package/skills/jgengine-world/visibility.md +139 -0
- package/skills/level-design/SKILL.md +1 -1
- package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
- package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
- package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
- package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { distance3, resolveEmitterGain } from "@jgengine/core/audio/audioFalloff";
|
|
2
|
+
import { dopplerRate } from "@jgengine/core/audio/doppler";
|
|
2
3
|
import { patchDuration } from "@jgengine/core/audio/synth";
|
|
4
|
+
import { createVoiceAllocator } from "@jgengine/core/audio/voiceAllocator";
|
|
3
5
|
import { createDisposer } from "@jgengine/core/game/defineGame";
|
|
4
|
-
import { clampLoopGain, clampLoopRate } from "./loopParams.js";
|
|
6
|
+
import { clampLoopCutoff, clampLoopGain, clampLoopRate, MIN_LOOP_CUTOFF, MAX_LOOP_CUTOFF } from "./loopParams.js";
|
|
5
7
|
import { MusicDirector } from "./musicDirector.js";
|
|
6
8
|
import { createNoiseBuffer, realizeSynthPatch } from "./synthEngine.js";
|
|
7
9
|
/** setTargetAtTime time constant (~20 ms) for zipper-free live rate/gain ramps on retained loops (#1051). */
|
|
8
10
|
const LOOP_PARAM_SMOOTH_TC = 0.02;
|
|
11
|
+
const ZERO_VELOCITY = { x: 0, y: 0, z: 0 };
|
|
9
12
|
function createNoopEngine() {
|
|
10
13
|
return {
|
|
11
14
|
setListenerPose: () => undefined,
|
|
15
|
+
setVoiceLimit: () => undefined,
|
|
12
16
|
playOneShot: () => undefined,
|
|
13
17
|
playLoop: () => null,
|
|
14
18
|
playMusic: () => undefined,
|
|
@@ -42,25 +46,38 @@ function setPannerPosition(panner, position) {
|
|
|
42
46
|
}
|
|
43
47
|
function createPanner(context, spatial, position) {
|
|
44
48
|
const panner = context.createPanner();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
try {
|
|
50
|
+
panner.panningModel = spatial.panning === "hrtf" ? "HRTF" : "equalpower";
|
|
51
|
+
panner.distanceModel = "inverse";
|
|
52
|
+
if (spatial.refDistance !== undefined)
|
|
53
|
+
panner.refDistance = spatial.refDistance;
|
|
54
|
+
if (spatial.maxDistance !== undefined)
|
|
55
|
+
panner.maxDistance = spatial.maxDistance;
|
|
56
|
+
if (spatial.rolloff !== undefined)
|
|
57
|
+
panner.rolloffFactor = spatial.rolloff;
|
|
58
|
+
if (spatial.coneInner !== undefined)
|
|
59
|
+
panner.coneInnerAngle = spatial.coneInner;
|
|
60
|
+
if (spatial.coneOuter !== undefined)
|
|
61
|
+
panner.coneOuterAngle = spatial.coneOuter;
|
|
62
|
+
if (spatial.coneOuterGain !== undefined)
|
|
63
|
+
panner.coneOuterGain = spatial.coneOuterGain;
|
|
64
|
+
setPannerPosition(panner, position);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
panner.disconnect();
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
60
70
|
return panner;
|
|
61
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Game-owned sound catalog playback with bounded SFX reservations, spatial panning, and shared buses.
|
|
74
|
+
* @capability bounded-spatial-audio play positional or flat cues with listener orientation, voice budgets, and per-source cleanup
|
|
75
|
+
*/
|
|
62
76
|
export function createAudioEngine(config = {}) {
|
|
63
77
|
const sounds = config.sounds ?? {};
|
|
78
|
+
const allocator = config.voiceAllocator ?? createVoiceAllocator({ maxTotal: config.maxVoices, overflow: config.voiceOverflow });
|
|
79
|
+
let disposed = false;
|
|
80
|
+
const activeVoices = new Map();
|
|
64
81
|
const busDefs = config.buses ?? {};
|
|
65
82
|
const AudioContextCtor = resolveAudioContextCtor();
|
|
66
83
|
if (AudioContextCtor === undefined)
|
|
@@ -80,9 +97,15 @@ export function createAudioEngine(config = {}) {
|
|
|
80
97
|
let node = busGains.get(busId);
|
|
81
98
|
if (node === undefined) {
|
|
82
99
|
node = context.createGain();
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
100
|
+
try {
|
|
101
|
+
node.gain.value = busDefs[busId]?.gain ?? 1;
|
|
102
|
+
node.connect(masterGain);
|
|
103
|
+
busGains.set(busId, node);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
node.disconnect();
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
86
109
|
}
|
|
87
110
|
return node;
|
|
88
111
|
}
|
|
@@ -140,36 +163,69 @@ export function createAudioEngine(config = {}) {
|
|
|
140
163
|
return pending;
|
|
141
164
|
}
|
|
142
165
|
let listenerPosition = { x: 0, y: 0, z: 0 };
|
|
166
|
+
let listenerVelocity = { x: 0, y: 0, z: 0 };
|
|
167
|
+
const nyquist = context.sampleRate / 2;
|
|
143
168
|
const activeSpatialUpdaters = new Set();
|
|
144
|
-
const activeLoops = new Set();
|
|
145
|
-
const NOOP_HANDLE = {
|
|
146
|
-
setPosition: () => undefined,
|
|
147
|
-
setRate: () => undefined,
|
|
148
|
-
setGain: () => undefined,
|
|
149
|
-
stop: () => undefined,
|
|
150
|
-
};
|
|
151
169
|
function playInternal(soundId, position, loop) {
|
|
170
|
+
if (disposed)
|
|
171
|
+
return null;
|
|
152
172
|
const sound = sounds[soundId];
|
|
153
|
-
if (sound === undefined)
|
|
173
|
+
if (sound === undefined || (sound.synth === undefined && sound.url === undefined))
|
|
154
174
|
return null;
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
175
|
+
const allocation = allocator.request(soundId, sound.priority, sound.maxVoices);
|
|
176
|
+
if (!allocation.ok)
|
|
177
|
+
return null;
|
|
178
|
+
for (const id of allocation.stolen)
|
|
179
|
+
activeVoices.get(id)?.();
|
|
180
|
+
const voiceId = allocation.voiceId;
|
|
181
|
+
let bus;
|
|
182
|
+
try {
|
|
183
|
+
bus = busGainNode(sound.bus);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
allocator.release(voiceId);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const currentPosition = { ...(position ?? listenerPosition) };
|
|
158
190
|
if (sound.synth !== undefined && !loop) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
panner
|
|
191
|
+
let cueGain = null;
|
|
192
|
+
let panner = null;
|
|
193
|
+
let playback = null;
|
|
194
|
+
let stopped = false;
|
|
195
|
+
function updateGain() {
|
|
196
|
+
if (cueGain === null)
|
|
197
|
+
return;
|
|
198
|
+
cueGain.gain.value = panner === null
|
|
199
|
+
? resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1)
|
|
200
|
+
: sound.gain ?? 1;
|
|
167
201
|
}
|
|
168
|
-
|
|
169
|
-
|
|
202
|
+
function stop() {
|
|
203
|
+
if (stopped)
|
|
204
|
+
return;
|
|
205
|
+
stopped = true;
|
|
206
|
+
activeVoices.delete(voiceId);
|
|
207
|
+
activeSpatialUpdaters.delete(updateGain);
|
|
208
|
+
allocator.release(voiceId);
|
|
209
|
+
playback?.stop();
|
|
210
|
+
cueGain?.disconnect();
|
|
211
|
+
panner?.disconnect();
|
|
170
212
|
}
|
|
171
|
-
|
|
172
|
-
|
|
213
|
+
activeVoices.set(voiceId, stop);
|
|
214
|
+
try {
|
|
215
|
+
cueGain = context.createGain();
|
|
216
|
+
const spatial = sound.positional === false ? undefined : sound.spatial;
|
|
217
|
+
panner = spatial === undefined ? null : createPanner(context, spatial, currentPosition);
|
|
218
|
+
updateGain();
|
|
219
|
+
cueGain.connect(panner ?? bus);
|
|
220
|
+
panner?.connect(bus);
|
|
221
|
+
playback = realizeSynthPatch(context, cueGain, sharedNoiseBuffer(), sound.synth);
|
|
222
|
+
activeSpatialUpdaters.add(updateGain);
|
|
223
|
+
playback.onEnded(stop);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
stop();
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
173
229
|
}
|
|
174
230
|
// Everything else resolves to one AudioBuffer we loop/play: sample URL, or a synth patch rendered once.
|
|
175
231
|
let bufferPromise;
|
|
@@ -179,79 +235,142 @@ export function createAudioEngine(config = {}) {
|
|
|
179
235
|
bufferPromise = loadBuffer(sound.url);
|
|
180
236
|
else
|
|
181
237
|
return null;
|
|
182
|
-
return playBufferSource(sound, bus, bufferPromise, currentPosition, loop);
|
|
238
|
+
return playBufferSource(sound, bus, bufferPromise, currentPosition, loop, voiceId);
|
|
183
239
|
}
|
|
184
|
-
function playBufferSource(sound, bus, bufferPromise, initialPosition, loop) {
|
|
240
|
+
function playBufferSource(sound, bus, bufferPromise, initialPosition, loop, voiceId) {
|
|
185
241
|
const disposer = createDisposer();
|
|
186
|
-
let currentPosition = initialPosition;
|
|
242
|
+
let currentPosition = { ...initialPosition };
|
|
243
|
+
let stopped = false;
|
|
244
|
+
const spatial = sound.positional === false ? undefined : sound.spatial;
|
|
187
245
|
// Live control state, applied on source creation so updates that race the async buffer load stick.
|
|
188
246
|
let currentRate = 1;
|
|
189
247
|
let currentUserGain = 1;
|
|
248
|
+
let currentLowpass = MAX_LOOP_CUTOFF;
|
|
249
|
+
let currentHighpass = MIN_LOOP_CUTOFF;
|
|
250
|
+
let currentVelocity = { x: 0, y: 0, z: 0 };
|
|
251
|
+
const dopplerFactor = loop ? sound.doppler ?? 0 : 0;
|
|
190
252
|
let sourceNode = null;
|
|
253
|
+
let lowpassNode = null;
|
|
254
|
+
let highpassNode = null;
|
|
191
255
|
let userGainNode = null;
|
|
192
256
|
let falloffGain = null;
|
|
193
257
|
let pannerNode = null;
|
|
258
|
+
function effectiveRate() {
|
|
259
|
+
if (dopplerFactor === 0)
|
|
260
|
+
return currentRate;
|
|
261
|
+
const shift = dopplerRate([listenerPosition.x, listenerPosition.y, listenerPosition.z], [listenerVelocity.x, listenerVelocity.y, listenerVelocity.z], [currentPosition.x, currentPosition.y, currentPosition.z], [currentVelocity.x, currentVelocity.y, currentVelocity.z], { factor: dopplerFactor });
|
|
262
|
+
return clampLoopRate(currentRate * shift);
|
|
263
|
+
}
|
|
264
|
+
function applyRate() {
|
|
265
|
+
if (sourceNode !== null)
|
|
266
|
+
sourceNode.playbackRate.setTargetAtTime(effectiveRate(), context.currentTime, LOOP_PARAM_SMOOTH_TC);
|
|
267
|
+
}
|
|
194
268
|
function updateFalloff() {
|
|
195
|
-
if (falloffGain !== null &&
|
|
269
|
+
if (falloffGain !== null && spatial === undefined) {
|
|
196
270
|
falloffGain.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
|
|
197
271
|
}
|
|
198
272
|
if (pannerNode !== null)
|
|
199
273
|
setPannerPosition(pannerNode, currentPosition);
|
|
274
|
+
if (dopplerFactor !== 0)
|
|
275
|
+
applyRate();
|
|
200
276
|
}
|
|
277
|
+
disposer.onDispose(() => {
|
|
278
|
+
if (sourceNode !== null)
|
|
279
|
+
sourceNode.onended = null;
|
|
280
|
+
try {
|
|
281
|
+
sourceNode?.stop();
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
}
|
|
285
|
+
sourceNode?.disconnect();
|
|
286
|
+
lowpassNode?.disconnect();
|
|
287
|
+
highpassNode?.disconnect();
|
|
288
|
+
userGainNode?.disconnect();
|
|
289
|
+
falloffGain?.disconnect();
|
|
290
|
+
pannerNode?.disconnect();
|
|
291
|
+
sourceNode = null;
|
|
292
|
+
lowpassNode = null;
|
|
293
|
+
highpassNode = null;
|
|
294
|
+
userGainNode = null;
|
|
295
|
+
falloffGain = null;
|
|
296
|
+
pannerNode = null;
|
|
297
|
+
});
|
|
201
298
|
void bufferPromise.then((buffer) => {
|
|
202
|
-
if (
|
|
299
|
+
if (stopped || disposed)
|
|
300
|
+
return;
|
|
301
|
+
if (buffer === null) {
|
|
302
|
+
handle.stop();
|
|
203
303
|
return;
|
|
304
|
+
}
|
|
204
305
|
const src = context.createBufferSource();
|
|
306
|
+
sourceNode = src;
|
|
205
307
|
src.buffer = buffer;
|
|
206
308
|
src.loop = loop || (sound.loop ?? false);
|
|
207
|
-
src.playbackRate.value =
|
|
208
|
-
//
|
|
309
|
+
src.playbackRate.value = effectiveRate();
|
|
310
|
+
// Spatial panners own distance attenuation; the scalar stage keeps only authored gain.
|
|
209
311
|
const uGain = context.createGain();
|
|
312
|
+
userGainNode = uGain;
|
|
210
313
|
uGain.gain.value = currentUserGain;
|
|
211
314
|
const fGain = context.createGain();
|
|
315
|
+
falloffGain = fGain;
|
|
212
316
|
fGain.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
|
|
213
|
-
|
|
317
|
+
if (loop) {
|
|
318
|
+
const lp = context.createBiquadFilter();
|
|
319
|
+
lowpassNode = lp;
|
|
320
|
+
lp.type = "lowpass";
|
|
321
|
+
lp.frequency.value = clampLoopCutoff(currentLowpass, MAX_LOOP_CUTOFF, nyquist);
|
|
322
|
+
const hp = context.createBiquadFilter();
|
|
323
|
+
highpassNode = hp;
|
|
324
|
+
hp.type = "highpass";
|
|
325
|
+
hp.frequency.value = clampLoopCutoff(currentHighpass, MIN_LOOP_CUTOFF, nyquist);
|
|
326
|
+
src.connect(lp);
|
|
327
|
+
lp.connect(hp);
|
|
328
|
+
hp.connect(uGain);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
src.connect(uGain);
|
|
332
|
+
}
|
|
214
333
|
uGain.connect(fGain);
|
|
215
|
-
if (
|
|
216
|
-
const panner = createPanner(context,
|
|
217
|
-
|
|
334
|
+
if (spatial !== undefined) {
|
|
335
|
+
const panner = createPanner(context, spatial, currentPosition);
|
|
336
|
+
pannerNode = panner;
|
|
337
|
+
fGain.gain.value = sound.gain ?? 1;
|
|
218
338
|
fGain.connect(panner);
|
|
219
339
|
panner.connect(bus);
|
|
220
|
-
pannerNode = panner;
|
|
221
340
|
}
|
|
222
341
|
else {
|
|
223
342
|
fGain.connect(bus);
|
|
224
343
|
}
|
|
344
|
+
src.onended = () => handle.stop();
|
|
225
345
|
src.start();
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
falloffGain = fGain;
|
|
229
|
-
disposer.onDispose(() => {
|
|
230
|
-
try {
|
|
231
|
-
src.stop();
|
|
232
|
-
}
|
|
233
|
-
catch {
|
|
234
|
-
}
|
|
235
|
-
src.disconnect();
|
|
236
|
-
uGain.disconnect();
|
|
237
|
-
fGain.disconnect();
|
|
238
|
-
pannerNode?.disconnect();
|
|
239
|
-
sourceNode = null;
|
|
240
|
-
userGainNode = null;
|
|
241
|
-
falloffGain = null;
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
if (loop)
|
|
245
|
-
activeSpatialUpdaters.add(updateFalloff);
|
|
346
|
+
}).catch(() => handle.stop());
|
|
347
|
+
activeSpatialUpdaters.add(updateFalloff);
|
|
246
348
|
const handle = {
|
|
349
|
+
isPlaying: () => !stopped,
|
|
247
350
|
setPosition(next) {
|
|
248
|
-
|
|
351
|
+
if (stopped)
|
|
352
|
+
return;
|
|
353
|
+
currentPosition = { ...next };
|
|
249
354
|
updateFalloff();
|
|
250
355
|
},
|
|
251
356
|
setRate(rate) {
|
|
252
357
|
currentRate = clampLoopRate(rate);
|
|
253
|
-
|
|
254
|
-
|
|
358
|
+
applyRate();
|
|
359
|
+
},
|
|
360
|
+
setLowpass(hz) {
|
|
361
|
+
currentLowpass = clampLoopCutoff(hz, MAX_LOOP_CUTOFF, nyquist);
|
|
362
|
+
if (lowpassNode !== null)
|
|
363
|
+
lowpassNode.frequency.setTargetAtTime(currentLowpass, context.currentTime, LOOP_PARAM_SMOOTH_TC);
|
|
364
|
+
},
|
|
365
|
+
setHighpass(hz) {
|
|
366
|
+
currentHighpass = clampLoopCutoff(hz, MIN_LOOP_CUTOFF, nyquist);
|
|
367
|
+
if (highpassNode !== null)
|
|
368
|
+
highpassNode.frequency.setTargetAtTime(currentHighpass, context.currentTime, LOOP_PARAM_SMOOTH_TC);
|
|
369
|
+
},
|
|
370
|
+
setVelocity(velocity) {
|
|
371
|
+
currentVelocity = { ...velocity };
|
|
372
|
+
if (dopplerFactor !== 0)
|
|
373
|
+
applyRate();
|
|
255
374
|
},
|
|
256
375
|
setGain(gain) {
|
|
257
376
|
currentUserGain = clampLoopGain(gain);
|
|
@@ -259,19 +378,31 @@ export function createAudioEngine(config = {}) {
|
|
|
259
378
|
userGainNode.gain.setTargetAtTime(currentUserGain, context.currentTime, LOOP_PARAM_SMOOTH_TC);
|
|
260
379
|
},
|
|
261
380
|
stop() {
|
|
381
|
+
if (stopped)
|
|
382
|
+
return;
|
|
383
|
+
stopped = true;
|
|
262
384
|
activeSpatialUpdaters.delete(updateFalloff);
|
|
263
|
-
|
|
385
|
+
activeVoices.delete(voiceId);
|
|
386
|
+
allocator.release(voiceId);
|
|
264
387
|
disposer.dispose();
|
|
265
388
|
},
|
|
266
389
|
};
|
|
267
|
-
|
|
268
|
-
activeLoops.add(handle);
|
|
390
|
+
activeVoices.set(voiceId, () => handle.stop());
|
|
269
391
|
return handle;
|
|
270
392
|
}
|
|
271
393
|
return {
|
|
394
|
+
setVoiceLimit(maxTotal, overflow) {
|
|
395
|
+
if (disposed)
|
|
396
|
+
return;
|
|
397
|
+
for (const id of allocator.retune({ maxTotal, overflow }))
|
|
398
|
+
activeVoices.get(id)?.();
|
|
399
|
+
},
|
|
272
400
|
setListenerPose(position) {
|
|
401
|
+
if (disposed)
|
|
402
|
+
return;
|
|
273
403
|
const pose = "position" in position ? position : { position, forward: { x: 0, y: 0, z: -1 }, up: { x: 0, y: 1, z: 0 } };
|
|
274
|
-
listenerPosition = pose.position;
|
|
404
|
+
listenerPosition = { ...pose.position };
|
|
405
|
+
listenerVelocity = "velocity" in pose && pose.velocity !== undefined ? { ...pose.velocity } : ZERO_VELOCITY;
|
|
275
406
|
const listener = context.listener;
|
|
276
407
|
setAudioParam(listener.positionX, pose.position.x);
|
|
277
408
|
setAudioParam(listener.positionY, pose.position.y);
|
|
@@ -286,6 +417,8 @@ export function createAudioEngine(config = {}) {
|
|
|
286
417
|
updateGain();
|
|
287
418
|
},
|
|
288
419
|
playOneShot(soundId, position) {
|
|
420
|
+
if (disposed)
|
|
421
|
+
return;
|
|
289
422
|
void context.resume().catch(() => undefined);
|
|
290
423
|
playInternal(soundId, position, false);
|
|
291
424
|
},
|
|
@@ -293,23 +426,38 @@ export function createAudioEngine(config = {}) {
|
|
|
293
426
|
return playInternal(soundId, position, true);
|
|
294
427
|
},
|
|
295
428
|
playMusic(themeId, options) {
|
|
429
|
+
if (disposed)
|
|
430
|
+
return;
|
|
296
431
|
void context.resume().catch(() => undefined);
|
|
297
432
|
musicDirector()?.crossfadeTo(themeId, options);
|
|
298
433
|
},
|
|
299
434
|
setBusGain(busId, gain) {
|
|
435
|
+
if (disposed)
|
|
436
|
+
return;
|
|
300
437
|
busGainNode(busId).gain.value = gain;
|
|
301
438
|
},
|
|
302
439
|
setMasterGain(gain) {
|
|
440
|
+
if (disposed)
|
|
441
|
+
return;
|
|
303
442
|
masterGain.gain.value = gain;
|
|
304
443
|
},
|
|
305
444
|
resume() {
|
|
445
|
+
if (disposed)
|
|
446
|
+
return;
|
|
306
447
|
void context.resume().catch(() => undefined);
|
|
307
448
|
},
|
|
308
449
|
dispose() {
|
|
309
|
-
|
|
310
|
-
|
|
450
|
+
if (disposed)
|
|
451
|
+
return;
|
|
452
|
+
disposed = true;
|
|
453
|
+
for (const stop of [...activeVoices.values()])
|
|
454
|
+
stop();
|
|
311
455
|
activeSpatialUpdaters.clear();
|
|
312
456
|
director?.dispose();
|
|
457
|
+
for (const node of busGains.values())
|
|
458
|
+
node.disconnect();
|
|
459
|
+
busGains.clear();
|
|
460
|
+
masterGain.disconnect();
|
|
313
461
|
void context.close().catch(() => undefined);
|
|
314
462
|
},
|
|
315
463
|
};
|
package/dist/audio/audioWire.js
CHANGED
|
@@ -19,8 +19,8 @@ export function attachAudioEventWire(events, audioEngine) {
|
|
|
19
19
|
const offLoopStart = events.on("audio.loopStart", ({ id, sound, at }) => {
|
|
20
20
|
const existing = loops.get(id);
|
|
21
21
|
if (existing !== undefined) {
|
|
22
|
-
//
|
|
23
|
-
if (existing.sound === sound)
|
|
22
|
+
// Keep active/pending loops; a stolen voice can retry on the next explicit start.
|
|
23
|
+
if (existing.sound === sound && existing.handle.isPlaying?.() !== false)
|
|
24
24
|
return;
|
|
25
25
|
existing.handle.stop();
|
|
26
26
|
loops.delete(id);
|
|
@@ -29,7 +29,7 @@ export function attachAudioEventWire(events, audioEngine) {
|
|
|
29
29
|
if (handle !== null)
|
|
30
30
|
loops.set(id, { sound, handle });
|
|
31
31
|
});
|
|
32
|
-
const offLoopSet = events.on("audio.loopSet", ({ id, rate, gain, at }) => {
|
|
32
|
+
const offLoopSet = events.on("audio.loopSet", ({ id, rate, gain, at, velocity, lowpass, highpass }) => {
|
|
33
33
|
const entry = loops.get(id);
|
|
34
34
|
// Unknown id: a live update may race a stop — silently drop it.
|
|
35
35
|
if (entry === undefined)
|
|
@@ -40,6 +40,12 @@ export function attachAudioEventWire(events, audioEngine) {
|
|
|
40
40
|
entry.handle.setGain(gain);
|
|
41
41
|
if (at !== undefined)
|
|
42
42
|
entry.handle.setPosition({ x: at[0], y: at[1], z: at[2] });
|
|
43
|
+
if (velocity !== undefined)
|
|
44
|
+
entry.handle.setVelocity({ x: velocity[0], y: velocity[1], z: velocity[2] });
|
|
45
|
+
if (lowpass !== undefined)
|
|
46
|
+
entry.handle.setLowpass(lowpass);
|
|
47
|
+
if (highpass !== undefined)
|
|
48
|
+
entry.handle.setHighpass(highpass);
|
|
43
49
|
});
|
|
44
50
|
const offLoopStop = events.on("audio.loopStop", ({ id }) => {
|
|
45
51
|
const entry = loops.get(id);
|
|
@@ -18,3 +18,42 @@ export declare function clampLoopRate(rate: number): number;
|
|
|
18
18
|
* @internal — the audio engine applies this; game code sets gain through `ctx.game.audio.setLoop`.
|
|
19
19
|
*/
|
|
20
20
|
export declare function clampLoopGain(gain: number): number;
|
|
21
|
+
/** Lowest live filter cutoff, Hz. */
|
|
22
|
+
export declare const MIN_LOOP_CUTOFF = 10;
|
|
23
|
+
/** Highest live filter cutoff, Hz; the engine also caps it at the context's Nyquist frequency. */
|
|
24
|
+
export declare const MAX_LOOP_CUTOFF = 22050;
|
|
25
|
+
/**
|
|
26
|
+
* Clamp a loop filter cutoff into `MIN_LOOP_CUTOFF`–`min(MAX_LOOP_CUTOFF, nyquist)` Hz. Non-finite input
|
|
27
|
+
* falls back to `fallback` (the open, transparent end of that filter).
|
|
28
|
+
* @internal — the audio engine applies this; game code sets cutoffs through `ctx.game.audio.setLoop`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function clampLoopCutoff(hz: number, fallback: number, nyquist?: number): number;
|
|
31
|
+
/** Smoothed listener velocity estimate from per-frame camera positions. @internal */
|
|
32
|
+
export interface ListenerVelocityTrack {
|
|
33
|
+
last: {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
z: number;
|
|
37
|
+
} | null;
|
|
38
|
+
velocity: {
|
|
39
|
+
x: number;
|
|
40
|
+
y: number;
|
|
41
|
+
z: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** @internal */
|
|
45
|
+
export declare function createListenerVelocityTrack(): ListenerVelocityTrack;
|
|
46
|
+
/**
|
|
47
|
+
* Advance the listener velocity estimate by one frame: the position delta over `dt`, eased with a
|
|
48
|
+
* ~50 ms time constant so frame-time jitter does not warble doppler. A cut resets it to zero.
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export declare function stepListenerVelocity(track: ListenerVelocityTrack, position: {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
z: number;
|
|
55
|
+
}, dt: number): {
|
|
56
|
+
x: number;
|
|
57
|
+
y: number;
|
|
58
|
+
z: number;
|
|
59
|
+
};
|
package/dist/audio/loopParams.js
CHANGED
|
@@ -26,3 +26,46 @@ export function clampLoopGain(gain) {
|
|
|
26
26
|
return 0;
|
|
27
27
|
return Math.min(1, Math.max(0, gain));
|
|
28
28
|
}
|
|
29
|
+
/** Lowest live filter cutoff, Hz. */
|
|
30
|
+
export const MIN_LOOP_CUTOFF = 10;
|
|
31
|
+
/** Highest live filter cutoff, Hz; the engine also caps it at the context's Nyquist frequency. */
|
|
32
|
+
export const MAX_LOOP_CUTOFF = 22050;
|
|
33
|
+
/**
|
|
34
|
+
* Clamp a loop filter cutoff into `MIN_LOOP_CUTOFF`–`min(MAX_LOOP_CUTOFF, nyquist)` Hz. Non-finite input
|
|
35
|
+
* falls back to `fallback` (the open, transparent end of that filter).
|
|
36
|
+
* @internal — the audio engine applies this; game code sets cutoffs through `ctx.game.audio.setLoop`.
|
|
37
|
+
*/
|
|
38
|
+
export function clampLoopCutoff(hz, fallback, nyquist = MAX_LOOP_CUTOFF) {
|
|
39
|
+
const max = Math.min(MAX_LOOP_CUTOFF, nyquist);
|
|
40
|
+
if (!Number.isFinite(hz))
|
|
41
|
+
return Math.min(max, Math.max(MIN_LOOP_CUTOFF, fallback));
|
|
42
|
+
return Math.min(max, Math.max(MIN_LOOP_CUTOFF, hz));
|
|
43
|
+
}
|
|
44
|
+
/** @internal */
|
|
45
|
+
export function createListenerVelocityTrack() {
|
|
46
|
+
return { last: null, velocity: { x: 0, y: 0, z: 0 } };
|
|
47
|
+
}
|
|
48
|
+
/** Camera jumps faster than this (world units/s) are cuts or teleports, not motion, and reset the estimate. */
|
|
49
|
+
const MAX_LISTENER_SPEED = 400;
|
|
50
|
+
/**
|
|
51
|
+
* Advance the listener velocity estimate by one frame: the position delta over `dt`, eased with a
|
|
52
|
+
* ~50 ms time constant so frame-time jitter does not warble doppler. A cut resets it to zero.
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
export function stepListenerVelocity(track, position, dt) {
|
|
56
|
+
const last = track.last;
|
|
57
|
+
track.last = { x: position.x, y: position.y, z: position.z };
|
|
58
|
+
if (last === null || !(dt > 0))
|
|
59
|
+
return track.velocity;
|
|
60
|
+
const vx = (position.x - last.x) / dt;
|
|
61
|
+
const vy = (position.y - last.y) / dt;
|
|
62
|
+
const vz = (position.z - last.z) / dt;
|
|
63
|
+
if (Math.hypot(vx, vy, vz) > MAX_LISTENER_SPEED) {
|
|
64
|
+
track.velocity = { x: 0, y: 0, z: 0 };
|
|
65
|
+
return track.velocity;
|
|
66
|
+
}
|
|
67
|
+
const blend = 1 - Math.exp(-dt / 0.05);
|
|
68
|
+
const v = track.velocity;
|
|
69
|
+
track.velocity = { x: v.x + (vx - v.x) * blend, y: v.y + (vy - v.y) * blend, z: v.z + (vz - v.z) * blend };
|
|
70
|
+
return track.velocity;
|
|
71
|
+
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { SynthPatch } from "@jgengine/core/audio/synth";
|
|
2
2
|
/** Build the shared 1-second mono white-noise buffer every noise voice samples from. */
|
|
3
3
|
export declare function createNoiseBuffer(ctx: BaseAudioContext): AudioBuffer;
|
|
4
|
+
/** Scheduled procedural sources; stopping releases their source/filter/envelope graph. */
|
|
5
|
+
export interface SynthPlayback {
|
|
6
|
+
stop(): void;
|
|
7
|
+
/** Called once after every source ends; late subscribers are notified immediately. */
|
|
8
|
+
onEnded(callback: () => void): void;
|
|
9
|
+
}
|
|
4
10
|
/**
|
|
5
11
|
* Realise a procedural cue on Web Audio: every voice is scheduled at
|
|
6
12
|
* `ctx.currentTime + delay` into `out`, summed into one one-shot. `noiseBuf` is
|
|
7
13
|
* the shared buffer from {@link createNoiseBuffer}.
|
|
8
14
|
*/
|
|
9
|
-
export declare function realizeSynthPatch(ctx: BaseAudioContext, out: AudioNode, noiseBuf: AudioBuffer, patch: SynthPatch):
|
|
15
|
+
export declare function realizeSynthPatch(ctx: BaseAudioContext, out: AudioNode, noiseBuf: AudioBuffer, patch: SynthPatch): SynthPlayback;
|