@vgai/engine 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -11
- package/package.json +10 -6
- package/schemas/engine-api.json +1 -68
- package/schemas/engine-api.md +1 -32
- package/schemas/engine-capabilities.json +30 -42
- package/schemas/{vgai-game.schema.json → vgai-project.schema.json} +52 -34
- package/src/adapter/adapter-surface.ts +5 -5
- package/src/adapter/authoring.ts +168 -226
- package/src/adapter/colyseus-networking-adapter.ts +44 -5
- package/src/adapter/first-party-systems.ts +156 -42
- package/src/adapter/host-context.ts +177 -46
- package/src/adapter/index.ts +47 -51
- package/src/adapter/ingest/game-contract.ts +2 -2
- package/src/adapter/ingest/scene-capture.ts +18 -19
- package/src/adapter/ingest/structural-ids.ts +127 -0
- package/src/adapter/ingest/upstream-pin.ts +9 -12
- package/src/adapter/loop-gate-report.ts +11 -11
- package/src/adapter/rapier-physics-adapter.ts +27 -9
- package/src/adapter/root-adapter.ts +217 -0
- package/src/adapter/{vgai-scene-game-adapter.ts → setup-three-root-adapter.ts} +173 -351
- package/src/adapter/system-adapter.ts +80 -63
- package/src/ai/navigation.ts +1 -1
- package/src/animation/animation-clock.ts +1 -1
- package/src/animation/camera-ownership.ts +1 -2
- package/src/animation/cubic-spline-interpolant.ts +132 -0
- package/src/animation/theatre-clock-binding.ts +2 -2
- package/src/animation/theatre-object-binding.ts +4 -4
- package/src/animation/xstate-animation-binding.ts +75 -5
- package/src/{scene/schema → asset-formats}/camera.ts +2 -5
- package/src/{scene/schema → asset-formats}/collider.ts +2 -5
- package/src/asset-formats/index.ts +54 -0
- package/src/{scene/schema → asset-formats}/instances.ts +10 -6
- package/src/{scene/schema → asset-formats}/light.ts +3 -6
- package/src/{scene/schema → asset-formats}/material.ts +4 -7
- package/src/{scene/schema → asset-formats}/mesh.ts +4 -7
- package/src/asset-formats/parse.ts +39 -0
- package/src/{scene/schema → asset-formats}/particles.ts +3 -6
- package/src/{scene/schema/environment.ts → asset-formats/render-env.ts} +23 -86
- package/src/{scene/schema → asset-formats}/tuples.ts +1 -1
- package/src/{scene/asset-loaders.ts → asset-loaders.ts} +8 -9
- package/src/asset-parse-error.ts +33 -0
- package/src/{scene/asset-registry.ts → asset-registry.ts} +1 -1
- package/src/assets.ts +1 -1
- package/src/audio/wav-encode.ts +9 -9
- package/src/canvas-react/engine-bridge.ts +59 -0
- package/src/canvas-react/index.ts +50 -0
- package/src/canvas-react/pixi-primitive.tsx +202 -0
- package/src/canvas-react/pixi-react-adapter.tsx +290 -0
- package/src/canvas-react/pixi-react-root-factory.tsx +88 -0
- package/src/canvas-react/world-context.ts +328 -0
- package/src/core/frame-pacing.ts +100 -0
- package/src/core/game-loop.ts +50 -28
- package/src/core/seeded-random.ts +7 -7
- package/src/core/sim-clock.ts +388 -0
- package/src/core/system-runner.ts +17 -63
- package/src/core/types.ts +34 -15
- package/src/data/data-asset.ts +3 -3
- package/src/data/data-check-core.ts +6 -7
- package/src/data/data-ref.ts +11 -11
- package/src/data/vite-plugin-data.ts +10 -10
- package/src/{scene/defaults.ts → defaults.ts} +18 -40
- package/src/dev/render-debug-adapter.ts +1 -1
- package/src/dev/webgl-frame-capture.ts +1 -1
- package/src/ecs/scene-index.ts +439 -0
- package/src/ecs/scene-query.ts +43 -0
- package/src/{scene → ecs}/user-data.ts +17 -36
- package/src/index.ts +7 -9
- package/src/input/input-manager.ts +29 -32
- package/src/input/input-types.ts +2 -2
- package/src/input/schema.ts +5 -5
- package/src/loader.ts +57 -0
- package/src/manifest/editor-port.ts +69 -0
- package/src/manifest/filename.ts +49 -0
- package/src/manifest/index.ts +8 -2
- package/src/manifest/load-file.ts +11 -0
- package/src/manifest/load.ts +65 -77
- package/src/manifest/locate.ts +55 -0
- package/src/manifest/schema.ts +400 -233
- package/src/{scene → physics}/collider-dimensions.ts +3 -3
- package/src/physics/physics-registry.ts +1 -1
- package/src/{world2d/authoring-2d.ts → pixi/authoring.ts} +24 -11
- package/src/pixi/index.ts +43 -0
- package/src/{world2d/ingest-iframe-2d.ts → pixi/ingest-iframe.ts} +9 -9
- package/src/{world2d/ingest2d.ts → pixi/ingest.ts} +28 -28
- package/src/{world2d/physics2d-registry.ts → pixi/physics-registry.ts} +1 -1
- package/src/{world2d/scene-capture-2d.ts → pixi/scene-capture.ts} +3 -3
- package/src/{world2d/system-adapters-2d.ts → pixi/system-adapters.ts} +2 -2
- package/src/react/unmanaged-root-detector.ts +26 -1
- package/src/react/use-data.ts +5 -5
- package/src/react/use-selection.tsx +15 -42
- package/src/react/{game-state.tsx → world-state.tsx} +44 -46
- package/src/render/auto-batcher.ts +1 -2
- package/src/{scene → render}/instance-mesh.ts +1 -1
- package/src/{scene → render}/light-camera-factory.ts +14 -13
- package/src/render/lod.ts +17 -0
- package/src/{scene → render}/material-factory.ts +8 -7
- package/src/{scene → render}/particles-factory.ts +62 -12
- package/src/render/render-batch-system.ts +14 -41
- package/src/render/render-features.ts +1 -1
- package/src/render/render-settings.ts +1 -2
- package/src/render/spark-renderer-lifecycle.ts +1 -1
- package/src/runtime/create-runtime.ts +352 -463
- package/src/runtime/debug-bridge.ts +148 -93
- package/src/runtime/debug-registry.ts +79 -54
- package/src/runtime/dev-layers.ts +40 -0
- package/src/runtime/frame-selector-cache.ts +4 -4
- package/src/runtime/game.ts +618 -369
- package/src/runtime/gameplay-rng-trap.ts +6 -7
- package/src/runtime/input-router.ts +11 -11
- package/src/runtime/mount-game.ts +54 -55
- package/src/runtime/mount-manifest.ts +154 -150
- package/src/runtime/presentation.ts +141 -0
- package/src/runtime/render-audio-control.ts +64 -53
- package/src/runtime/render-control.ts +45 -51
- package/src/runtime/render-seed.ts +3 -4
- package/src/runtime/state-bridge.ts +17 -18
- package/src/runtime/types.ts +94 -61
- package/src/setup/setup-renderer.ts +11 -6
- package/src/world3d-react/engine-bridge.ts +46 -33
- package/src/world3d-react/index.ts +31 -26
- package/src/world3d-react/r3f-adapter.tsx +211 -96
- package/src/world3d-react/r3f-root-factory.tsx +91 -0
- package/src/world3d-react/renderer-config.ts +137 -0
- package/src/world3d-react/world-context.ts +141 -111
- package/schemas/entity2d.schema.json +0 -468
- package/schemas/prefab.schema.json +0 -9992
- package/schemas/scn2d.schema.json +0 -494
- package/schemas/vscn.schema.json +0 -10851
- package/src/adapter/game-adapter.ts +0 -164
- package/src/adapter/ingest/overlay-applier.ts +0 -207
- package/src/adapter/ingest/overlay-apply.ts +0 -168
- package/src/adapter/ingest/overlay-file.ts +0 -126
- package/src/adapter/ingest/overlay-report.ts +0 -176
- package/src/animation/gsap-registration.ts +0 -184
- package/src/audio/audio-introspection.ts +0 -290
- package/src/audio/index.ts +0 -39
- package/src/audio/tone-clock-binding.ts +0 -98
- package/src/audio/tone-context.ts +0 -175
- package/src/audio/tone-offline-render.ts +0 -167
- package/src/ecs/component-manager.ts +0 -814
- package/src/ecs/game-component.ts +0 -260
- package/src/ecs/hmr-swap-report.ts +0 -65
- package/src/physics/trigger-dispatch.ts +0 -97
- package/src/react/root-adapter.tsx +0 -49
- package/src/scene/asset-paths.ts +0 -121
- package/src/scene/asset-ref-check.ts +0 -248
- package/src/scene/component-registry.ts +0 -51
- package/src/scene/parse.ts +0 -204
- package/src/scene/scene-apply.ts +0 -407
- package/src/scene/scene-diff-schema.ts +0 -115
- package/src/scene/scene-diff-types.ts +0 -29
- package/src/scene/scene-loader.ts +0 -1526
- package/src/scene/scene-query.ts +0 -63
- package/src/scene/scene-types.ts +0 -33
- package/src/scene/scene-version.ts +0 -40
- package/src/scene/schema/animation.ts +0 -46
- package/src/scene/schema/audio.ts +0 -25
- package/src/scene/schema/entity-ref.ts +0 -78
- package/src/scene/schema/entity.ts +0 -189
- package/src/scene/schema/index.ts +0 -51
- package/src/scene/schema/joint.ts +0 -26
- package/src/scene/schema/physics.ts +0 -49
- package/src/scene/schema/scene-file.ts +0 -292
- package/src/scene/schema/shadow.ts +0 -24
- package/src/scene/schema/spline.ts +0 -21
- package/src/world2d/asset-paths2d.ts +0 -44
- package/src/world2d/capture-to-scene2d.ts +0 -52
- package/src/world2d/collision-2d.ts +0 -99
- package/src/world2d/entity2d-asset.ts +0 -22
- package/src/world2d/index.ts +0 -91
- package/src/world2d/physics2d-transform.ts +0 -173
- package/src/world2d/physics2d-units.ts +0 -10
- package/src/world2d/pixi-game-adapter.ts +0 -439
- package/src/world2d/pixi-surface.ts +0 -78
- package/src/world2d/scene2d-identity.ts +0 -49
- package/src/world2d/scene2d-loader.ts +0 -433
- package/src/world2d/schema/entity2d.ts +0 -163
- package/src/world2d/schema/physics2d.ts +0 -64
- package/src/world2d/schema/sprite.ts +0 -99
- package/src/world2d/schema/tilemap.ts +0 -39
- package/src/world2d/schema/tuples2d.ts +0 -25
- package/src/world2d/transform-writer-2d.ts +0 -42
- package/src/world2d/types.ts +0 -74
- package/src/world3d-react/behavior.tsx +0 -146
- /package/src/{scene → render}/mesh-shadow.ts +0 -0
package/src/audio/index.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// Engine-core audio surface (spec §13 workstream G).
|
|
2
|
-
//
|
|
3
|
-
// G3 ("Replace or Relocate Procedural Audio Helpers") removed the former
|
|
4
|
-
// `musical.ts`/`weapons.ts`/`impacts.ts`/`vehicle.ts`/`ambient.ts`/
|
|
5
|
-
// `movement.ts`/`ui-sounds.ts` — raw-`AudioContext`/`Math.random`-driven
|
|
6
|
-
// oscillator and noise-buffer SFX generators that were engine-owned but
|
|
7
|
-
// non-spatial (they never touched `THREE.PositionalAudio`, so the "spatial
|
|
8
|
-
// one-shot helper" exception in the G3 acceptance criteria does not apply to
|
|
9
|
-
// them). Each was either relocated verbatim into the example project that
|
|
10
|
-
// used it (`examples/{third-person,fps,third-person-arena,rts,vehicle}/src/
|
|
11
|
-
// procedural-audio.ts`) or deleted as dead code with no shipped consumer
|
|
12
|
-
// (`bassPulse`/`arpeggio`/`gunshot`/`laser`/`bounce`/`crash`/`explosion`/
|
|
13
|
-
// `tireSkid`/`wind`/`rain`/`hum`/`nature`/`uiHover`/`uiError`). The canonical
|
|
14
|
-
// replacements — a procedural-music example built directly on Tone's own API
|
|
15
|
-
// and a spatial-audio example built directly on `THREE.PositionalAudio` — are
|
|
16
|
-
// `examples/audio-showcase`.
|
|
17
|
-
//
|
|
18
|
-
// What remains here is the genuine engine-core audio capability: the G1/G2
|
|
19
|
-
// Tone↔Three context bridge and offline render (`wav-encode.ts`, the G4
|
|
20
|
-
// PCM->WAV encoder, is consumed by its own module path, not this barrel — see
|
|
21
|
-
// `packages/engine/src/runtime/render-audio-control.ts`).
|
|
22
|
-
export {
|
|
23
|
-
bindClockToTone,
|
|
24
|
-
type ToneClockBinding,
|
|
25
|
-
type ToneClockBindingOptions,
|
|
26
|
-
type ToneTransportLike,
|
|
27
|
-
} from './tone-clock-binding';
|
|
28
|
-
export {
|
|
29
|
-
bridgeToneToThreeAudio,
|
|
30
|
-
connectToneBusToMasterGain,
|
|
31
|
-
resumeSharedAudio,
|
|
32
|
-
type ToneAudioBridge,
|
|
33
|
-
} from './tone-context';
|
|
34
|
-
export {
|
|
35
|
-
renderToneOffline,
|
|
36
|
-
type ToneOfflineComposeFn,
|
|
37
|
-
type ToneOfflineRenderRequest,
|
|
38
|
-
type ToneOfflineRenderResult,
|
|
39
|
-
} from './tone-offline-render';
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import type { AnimationClock, ClockEvaluatorHandle } from '../animation/animation-clock';
|
|
2
|
-
import type { SystemPhaseName } from '../core/types';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* G2 — Integrate Tone with the Canonical Clock, preview/real-time half
|
|
6
|
-
* (spec §13 G2, §10 D1). Mirrors `animation/theatre-clock-binding.ts`'s D2
|
|
7
|
-
* pattern exactly, on purpose: the canonical `AnimationClock` is the single
|
|
8
|
-
* source of truth for "what time is it", and every participating system
|
|
9
|
-
* (Theatre's sequence position, a registered GSAP timeline, and — here — a
|
|
10
|
-
* Tone transport) is seeked to match it on every clock crossing. Tone must
|
|
11
|
-
* never be left to free-run its own real-time clock while participating in
|
|
12
|
-
* a cinematic, for the exact same reason Theatre isn't: deterministic
|
|
13
|
-
* export, offline scrubbing, and no wall-clock dependency
|
|
14
|
-
* (`animation-clock.ts`'s module doc explains why in full).
|
|
15
|
-
*
|
|
16
|
-
* ## Why setting `transport.seconds` is enough
|
|
17
|
-
*
|
|
18
|
-
* Setting `Tone.getTransport().seconds` repositions the transport's
|
|
19
|
-
* playhead synchronously; it does not itself start real-time ticking (that
|
|
20
|
-
* still requires an explicit `transport.start()`, which this binding
|
|
21
|
-
* deliberately never calls — see the module doc on `animation-clock.ts` for
|
|
22
|
-
* why nothing in this codebase is allowed to drive its own
|
|
23
|
-
* `requestAnimationFrame`/wall-clock loop). A clock-seeked Tone transport is
|
|
24
|
-
* a pure function of `clock.time`, exactly like a Theatre sequence's
|
|
25
|
-
* `position` setter.
|
|
26
|
-
*
|
|
27
|
-
* ## Structural (not nominal) transport typing
|
|
28
|
-
*
|
|
29
|
-
* `ToneTransportLike` intentionally types only the ONE property this
|
|
30
|
-
* binding touches (`seconds`), the same narrowing discipline
|
|
31
|
-
* `runtime/render-control.ts`'s `SeekableClock` uses for the canonical
|
|
32
|
-
* clock. `Tone.getTransport()`'s real `TransportClass` satisfies this
|
|
33
|
-
* structurally with no adapter — pass it directly at the real call site.
|
|
34
|
-
* The narrow type is also what makes this binding's own unit tests real
|
|
35
|
-
* (not mocked-away): `packages/engine/test/audio/tone-clock-binding.test.ts`
|
|
36
|
-
* exercises the FULL seek/pause/loop/time-scale matrix against a real
|
|
37
|
-
* `AnimationClock` and a plain `{ seconds: number }` object — no Tone
|
|
38
|
-
* import, no `AudioContext`, and it still proves the exact behavior a real
|
|
39
|
-
* `Tone.Transport` will see, because `TransportClass` IS a
|
|
40
|
-
* `ToneTransportLike` at the type level.
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
export interface ToneTransportLike {
|
|
44
|
-
seconds: number;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface ToneClockBindingOptions {
|
|
48
|
-
/**
|
|
49
|
-
* Which `AnimationClock` phase to seek the transport in. Defaults to
|
|
50
|
-
* `'animation'` — the same phase `theatre-clock-binding.ts` uses by
|
|
51
|
-
* default, so a Theatre sequence and a Tone transport bound to the same
|
|
52
|
-
* clock stay in lockstep with each other (`PHASE_ORDER`-then-registration-
|
|
53
|
-
* order — see `animation-clock.ts`'s `subscribe` doc comment).
|
|
54
|
-
*/
|
|
55
|
-
phase?: SystemPhaseName;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface ToneClockBinding {
|
|
59
|
-
/** Removes the clock evaluator. No further `transport.seconds` writes
|
|
60
|
-
* occur after this — the transport is left at whatever position it was
|
|
61
|
-
* last seeked to (matches `TheatreClockBinding.dispose`'s contract). */
|
|
62
|
-
dispose(): void;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Subscribe `transport.seconds = clock.time` to every crossing of `clock`
|
|
67
|
-
* in the given phase (default `'animation'`). Returns a disposer; call it
|
|
68
|
-
* to stop driving the transport (e.g. on `GameCleanup.dispose()`, or before
|
|
69
|
-
* rebinding on hot reload — this is what keeps HMR from accumulating
|
|
70
|
-
* duplicate listeners).
|
|
71
|
-
*
|
|
72
|
-
* Only call this for a Tone composition that PARTICIPATES in a cinematic
|
|
73
|
-
* (spec G2 AC). Ordinary, non-cinematic game music should drive Tone's own
|
|
74
|
-
* real-time transport directly (`transport.start()`, `Tone.Transport.bpm`,
|
|
75
|
-
* etc. — see `tone-context.ts`'s "escape hatch" section) and must NEVER be
|
|
76
|
-
* bound here, since a bound transport's position is entirely clock-driven
|
|
77
|
-
* and `start()`/`stop()` calls on it would fight this binding's writes.
|
|
78
|
-
*/
|
|
79
|
-
export function bindClockToTone(
|
|
80
|
-
clock: AnimationClock,
|
|
81
|
-
transport: ToneTransportLike,
|
|
82
|
-
options: ToneClockBindingOptions = {},
|
|
83
|
-
): ToneClockBinding {
|
|
84
|
-
const phase = options.phase ?? 'animation';
|
|
85
|
-
|
|
86
|
-
const handle: ClockEvaluatorHandle = clock.subscribe(phase, (crossing) => {
|
|
87
|
-
transport.seconds = crossing.currentTime;
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
let disposed = false;
|
|
91
|
-
return {
|
|
92
|
-
dispose(): void {
|
|
93
|
-
if (disposed) return;
|
|
94
|
-
disposed = true;
|
|
95
|
-
handle.dispose();
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import type { ToneAudioNode } from 'tone';
|
|
2
|
-
import * as Tone from 'tone';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* G1 — Establish Audio Ownership (spec §13 G1, §3.6, §4).
|
|
6
|
-
*
|
|
7
|
-
* ## The boundary
|
|
8
|
-
*
|
|
9
|
-
* Two independent, native-vocabulary audio systems coexist in every VGAI
|
|
10
|
-
* game, each with a genuinely different job. This module does not wrap
|
|
11
|
-
* either one — it only decides who owns which native `AudioContext` and how
|
|
12
|
-
* the two graphs meet:
|
|
13
|
-
*
|
|
14
|
-
* - **Three/Web Audio** (`setup/setup-audio.ts`) remains authoritative for
|
|
15
|
-
* SPATIAL audio: `THREE.AudioListener` (attached to the active camera)
|
|
16
|
-
* and `THREE.Audio`/`THREE.PositionalAudio` sources. (G3, a later unit,
|
|
17
|
-
* has since removed the non-spatial engine-owned one-shot procedural
|
|
18
|
-
* helpers this comment used to name here — `audio/impacts.ts`,
|
|
19
|
-
* `audio/weapons.ts`, `audio/ambient.ts`, and their siblings — relocating
|
|
20
|
-
* each into the example project that used it, or deleting it as dead code;
|
|
21
|
-
* see `packages/engine/src/audio/index.ts`'s header comment and
|
|
22
|
-
* `examples/audio-showcase` for the canonical native-Tone/native-Three
|
|
23
|
-
* replacements. This module's own boundary/bridge is unchanged by that
|
|
24
|
-
* unit.)
|
|
25
|
-
* - **Tone.js** owns musical transport, tempo, scheduling, synthesis, and
|
|
26
|
-
* procedural composition — imported and used with its OWN native API
|
|
27
|
-
* (`new Tone.Synth()`, `Tone.getTransport()`, `Tone.Offline(...)`,
|
|
28
|
-
* `Tone.getContext()`), never through a VGAI-renamed wrapper (spec §1's
|
|
29
|
-
* native-library rule). See `packages/engine/test/audio/tone-context.test.ts`
|
|
30
|
-
* for a direct, unwrapped usage example.
|
|
31
|
-
*
|
|
32
|
-
* ## Why they must share one native `AudioContext`
|
|
33
|
-
*
|
|
34
|
-
* Both systems ultimately talk to exactly one hardware output. If Tone ran
|
|
35
|
-
* its own independently-created `AudioContext` alongside
|
|
36
|
-
* `THREE.AudioListener`'s, they would: fight over autoplay-policy resume
|
|
37
|
-
* state (two contexts, two `state`s, two places a "click to start audio"
|
|
38
|
-
* gesture would need to unlock); produce TWO separate graphs that a single
|
|
39
|
-
* mute/volume control cannot coherently silence; and — fatally for G4/I6 —
|
|
40
|
-
* make deterministic offline capture impossible to align with the video's
|
|
41
|
-
* frame-addressed render range, since there would be no single graph whose
|
|
42
|
-
* lifecycle (suspend/resume/close) the render/capture harness can reason
|
|
43
|
-
* about.
|
|
44
|
-
*
|
|
45
|
-
* `bridgeToneToThreeAudio` fixes this: it points Tone's global context
|
|
46
|
-
* (`Tone.setContext`) at the SAME native `AudioContext` instance
|
|
47
|
-
* `THREE.AudioListener` already created (`setupAudio(camera).listener.context`,
|
|
48
|
-
* `setup/setup-audio.ts`). After bridging,
|
|
49
|
-
* `Tone.getContext().rawContext === threeListener.context` — there is
|
|
50
|
-
* exactly one native context. One `.resume()`/`.suspend()` call (or
|
|
51
|
-
* `Tone.start()`, which does both) affects both systems, and routing a Tone
|
|
52
|
-
* bus into `setupAudio()`'s `masterGain` (see `connectToneBusToMasterGain`
|
|
53
|
-
* below) puts Tone's output under the SAME mute/master-volume control as
|
|
54
|
-
* every `THREE.Audio` source.
|
|
55
|
-
*
|
|
56
|
-
* ## What this module does NOT do
|
|
57
|
-
*
|
|
58
|
-
* It does not create an `AudioContext` (that remains `setupAudio`'s job —
|
|
59
|
-
* call `bridgeToneToThreeAudio` AFTER `setupAudio(camera)`, before
|
|
60
|
-
* constructing any Tone node: Tone nodes built against the PREVIOUS global
|
|
61
|
-
* context are not retroactively migrated — Tone's own `setContext` contract,
|
|
62
|
-
* not a VGAI limitation). It does not wrap any Tone class, and it does not
|
|
63
|
-
* touch THREE's audio path at all. G3 (a later unit) is where engine-owned
|
|
64
|
-
* procedural helpers get rewritten directly on Tone or relocated to game
|
|
65
|
-
* code — this module only owns the context boundary + bridge.
|
|
66
|
-
*
|
|
67
|
-
* ## Escape hatch — ordinary (non-cinematic) game music
|
|
68
|
-
*
|
|
69
|
-
* None of the above requires participating in a cinematic. Once bridged,
|
|
70
|
-
* ordinary game music can drive Tone's REAL real-time transport exactly as
|
|
71
|
-
* Tone's own docs describe — `Tone.getTransport().bpm.value = 128`,
|
|
72
|
-
* `Tone.getTransport().start()`, `new Tone.Sequence(...).start(0)`, etc. —
|
|
73
|
-
* with no VGAI involvement at all. `tone-clock-binding.ts` (G2) is opt-in,
|
|
74
|
-
* only for the case where a Tone composition must stay in lockstep with the
|
|
75
|
-
* canonical `AnimationClock` because it participates in a cinematic.
|
|
76
|
-
*/
|
|
77
|
-
|
|
78
|
-
/** Result of {@link bridgeToneToThreeAudio} — the shared native context plus
|
|
79
|
-
* a re-checkable assertion, so a caller/test doesn't need to re-derive the
|
|
80
|
-
* bridging logic to confirm it still holds later. */
|
|
81
|
-
export interface ToneAudioBridge {
|
|
82
|
-
/** The native `AudioContext` Tone and `THREE.AudioListener` now share. */
|
|
83
|
-
readonly context: AudioContext;
|
|
84
|
-
/** True iff `Tone.getContext().rawContext` is still exactly `context`
|
|
85
|
-
* (identity, not equality — proves ONE shared native context, not two
|
|
86
|
-
* compatible-looking ones). */
|
|
87
|
-
isBridged(): boolean;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Point Tone's global context at `context` (typically
|
|
92
|
-
* `setupAudio(camera).listener.context` from `setup/setup-audio.ts`). Call
|
|
93
|
-
* this once per game boot, after `setupAudio(camera)`, before constructing
|
|
94
|
-
* any Tone node.
|
|
95
|
-
*/
|
|
96
|
-
export function bridgeToneToThreeAudio(context: AudioContext): ToneAudioBridge {
|
|
97
|
-
Tone.setContext(context);
|
|
98
|
-
bridgedContexts.add(context);
|
|
99
|
-
return {
|
|
100
|
-
context,
|
|
101
|
-
isBridged: () => Tone.getContext().rawContext === context,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// --- W3c introspection bookkeeping (read-only debugger seam) ----------------
|
|
106
|
-
//
|
|
107
|
-
// The registries below record what already flows through THIS module's two
|
|
108
|
-
// seams — nothing new is wrapped; Tone stays used through its own API. They
|
|
109
|
-
// exist so the first-party `AudioAdapter` introspection
|
|
110
|
-
// (`audio-introspection.ts`) can answer "is Tone bridged onto this world's
|
|
111
|
-
// context?" and "which Tone nodes feed this world's master bus?" TRUTHFULLY,
|
|
112
|
-
// without ever instantiating a Tone global context of its own (calling
|
|
113
|
-
// `Tone.getContext()` on a page whose game never used Tone would fabricate
|
|
114
|
-
// one just by asking).
|
|
115
|
-
|
|
116
|
-
const bridgedContexts = new WeakSet<AudioContext>();
|
|
117
|
-
|
|
118
|
-
/** `true` iff {@link bridgeToneToThreeAudio} has run for this exact native
|
|
119
|
-
* context. A cheap pre-check: callers still confirm the bridge is CURRENT
|
|
120
|
-
* via `Tone.getContext().rawContext === context` (a later re-bridge to a
|
|
121
|
-
* different context leaves stale entries here) — but only after this
|
|
122
|
-
* returns `true`, so the confirmation never forces Tone to create a
|
|
123
|
-
* context that didn't already exist. */
|
|
124
|
-
export function hasToneBridge(context: AudioContext): boolean {
|
|
125
|
-
return bridgedContexts.has(context);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
interface ToneMasterRoute {
|
|
129
|
-
node: ToneAudioNode;
|
|
130
|
-
masterGain: GainNode;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const toneMasterRoutes: ToneMasterRoute[] = [];
|
|
134
|
-
|
|
135
|
-
/** The still-alive Tone nodes routed into `masterGain` via
|
|
136
|
-
* {@link connectToneBusToMasterGain}. Disposed nodes (Tone's own
|
|
137
|
-
* `.disposed` flag) are pruned on every call, so a game that cleans up
|
|
138
|
-
* after itself leaves nothing behind here. Filtered by `masterGain`
|
|
139
|
-
* IDENTITY: one world's snapshot never shows another world's routes. */
|
|
140
|
-
export function getToneMasterGainRoutes(masterGain: GainNode): ToneAudioNode[] {
|
|
141
|
-
for (let i = toneMasterRoutes.length - 1; i >= 0; i--) {
|
|
142
|
-
if (toneMasterRoutes[i]?.node.disposed) toneMasterRoutes.splice(i, 1);
|
|
143
|
-
}
|
|
144
|
-
return toneMasterRoutes.filter((r) => r.masterGain === masterGain).map((r) => r.node);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Route a Tone output node (a bus, `Tone.getDestination()`, or any
|
|
149
|
-
* `Tone.Gain` used as a submix) into the SAME `masterGain`
|
|
150
|
-
* `setupAudio()` created, so Tone output is silenced/attenuated by the
|
|
151
|
-
* identical mute/volume control as every `THREE.Audio`/`THREE.PositionalAudio`
|
|
152
|
-
* source. `toneNode` is Tone's own `ToneAudioNode` type (no VGAI wrapper);
|
|
153
|
-
* `masterGain` is the plain native `GainNode` `setupAudio()` already
|
|
154
|
-
* returns — Tone's `.connect()` natively accepts a raw `AudioNode` as its
|
|
155
|
-
* destination, so no adapter node is needed on either side.
|
|
156
|
-
*/
|
|
157
|
-
export function connectToneBusToMasterGain(toneNode: ToneAudioNode, masterGain: GainNode): void {
|
|
158
|
-
toneNode.connect(masterGain);
|
|
159
|
-
// W3c: record the route so the Audio debugger's graph snapshot can show it
|
|
160
|
-
// (see the introspection-bookkeeping section below).
|
|
161
|
-
toneMasterRoutes.push({ node: toneNode, masterGain });
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Resume the shared context after a user gesture. Because
|
|
166
|
-
* {@link bridgeToneToThreeAudio} guarantees Tone and THREE share the
|
|
167
|
-
* identical native context, calling `Tone.start()` — which both resumes the
|
|
168
|
-
* context AND runs Tone's own autoplay-unlock bookkeeping (a silent-buffer
|
|
169
|
-
* trick required on some browsers) — is sufficient to unstick BOTH systems;
|
|
170
|
-
* a separate `context.resume()` call on the THREE side is redundant once
|
|
171
|
-
* bridged, not required.
|
|
172
|
-
*/
|
|
173
|
-
export async function resumeSharedAudio(): Promise<void> {
|
|
174
|
-
await Tone.start();
|
|
175
|
-
}
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { Offline } from 'tone';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* G2 — Integrate Tone with the Canonical Clock, OFFLINE-render half (spec
|
|
5
|
-
* §13 G2, and the G4/I6 seam it is deliberately shaped for). This module
|
|
6
|
-
* renders a Tone composition over an exact `[start, end)` time range to a
|
|
7
|
-
* real `AudioBuffer` via `Tone.Offline` — i.e. `OfflineAudioContext`, NOT a
|
|
8
|
-
* live real-time `AudioContext` — such that identical inputs produce
|
|
9
|
-
* bit-identical output samples on every call. This is what makes offline
|
|
10
|
-
* Tone audio safe to mux into a deterministically-rendered video (G4/I6):
|
|
11
|
-
* the video side already re-renders frame-by-frame from an exact clock
|
|
12
|
-
* position, and this is the audio side's equivalent — an exact re-render
|
|
13
|
-
* from an exact time range, not a live capture of whatever happened to play.
|
|
14
|
-
*
|
|
15
|
-
* ## Why this can't run in plain Node (documented, not assumed)
|
|
16
|
-
*
|
|
17
|
-
* `Tone.Offline` ultimately constructs a real `OfflineAudioContext`
|
|
18
|
-
* (via `standardized-audio-context`, Tone's own dependency). Node has no
|
|
19
|
-
* native Web Audio implementation, so `standardized-audio-context` throws
|
|
20
|
-
* `"Missing the native OfflineAudioContext constructor."` the moment
|
|
21
|
-
* `Tone.Offline(...)` is actually invoked — confirmed empirically against
|
|
22
|
-
* this repo's installed `tone@15.1.22` before writing this module (plain
|
|
23
|
-
* `import * as Tone from 'tone'` succeeds; calling `Tone.Offline(...)` in a
|
|
24
|
-
* Node process throws immediately). There is no lightweight polyfill in this
|
|
25
|
-
* repo's dependency tree, and installing a native Node Web Audio addon is
|
|
26
|
-
* out of scope for this unit (build-box constraints). The determinism proof
|
|
27
|
-
* for this module therefore runs in a REAL headless-Chromium page via
|
|
28
|
-
* Playwright — see `packages/engine/e2e/tests/tone-audio-g1-g2.spec.ts`
|
|
29
|
-
* — mirroring how I0 proved video-frame determinism. This module's own
|
|
30
|
-
* `packages/engine/test/audio/tone-offline-render.test.ts` covers everything
|
|
31
|
-
* that does NOT require a real `OfflineAudioContext` (range validation,
|
|
32
|
-
* synchronous pre-render cancellation) directly in Node.
|
|
33
|
-
*
|
|
34
|
-
* ## Composition contract
|
|
35
|
-
*
|
|
36
|
-
* `compose` is called by `Tone.Offline` itself, inside the offline context —
|
|
37
|
-
* build the SAME graph you would in real-time Tone code
|
|
38
|
-
* (`new Tone.Synth().toDestination()`, `context.transport.schedule(...)`,
|
|
39
|
-
* etc; see Tone's own `Offline` docs). Nothing about this wrapper changes
|
|
40
|
-
* Tone's native API surface — `compose`'s parameter is `Tone.Offline`'s own
|
|
41
|
-
* callback type, re-derived structurally (`Parameters<typeof Offline>[0]`)
|
|
42
|
-
* rather than duplicated, so this module can never drift from Tone's actual
|
|
43
|
-
* signature.
|
|
44
|
-
*
|
|
45
|
-
* ## Cancellation — cooperative, not a true mid-render abort
|
|
46
|
-
*
|
|
47
|
-
* The Web Audio spec gives `OfflineAudioContext` no `.close()`/cancel API
|
|
48
|
-
* once `startRendering()` is in flight, and neither does Tone. `signal`
|
|
49
|
-
* therefore provides COOPERATIVE cancellation only: if `signal` is already
|
|
50
|
-
* aborted before this function calls `Tone.Offline`, the render never
|
|
51
|
-
* starts at all (checked synchronously, before any `OfflineAudioContext` is
|
|
52
|
-
* created). If `signal` aborts WHILE a render is in flight, this function's
|
|
53
|
-
* returned promise rejects immediately with an `AbortError` — so a caller
|
|
54
|
-
* (e.g. a render-cancellation UI action) sees the render "stop" from its own
|
|
55
|
-
* perspective and never receives/uses the resulting buffer — even though
|
|
56
|
-
* the underlying browser-native render may keep running in the background
|
|
57
|
-
* until it would have finished anyway. This is an honest, documented
|
|
58
|
-
* limitation, not a gap: no browser API makes a stronger guarantee possible.
|
|
59
|
-
*/
|
|
60
|
-
|
|
61
|
-
/** Tone's own `Offline` callback signature, re-derived (not re-typed) so
|
|
62
|
-
* this module can never drift from the real function it wraps. */
|
|
63
|
-
export type ToneOfflineComposeFn = Parameters<typeof Offline>[0];
|
|
64
|
-
|
|
65
|
-
export interface ToneOfflineRenderRequest {
|
|
66
|
-
/** Canonical/Theatre-time range start, in seconds. */
|
|
67
|
-
readonly start: number;
|
|
68
|
-
/** Canonical/Theatre-time range end, in seconds. Must be `> start`. */
|
|
69
|
-
readonly end: number;
|
|
70
|
-
/** Output sample rate. Default 44100 (CD-quality, FFmpeg's usual default). */
|
|
71
|
-
readonly sampleRate?: number;
|
|
72
|
-
/** Output channel count. Default 2 (stereo). */
|
|
73
|
-
readonly channels?: number;
|
|
74
|
-
/** Builds the Tone graph inside the offline context — Tone's native
|
|
75
|
-
* `Offline` callback, unmodified. Receives local time `0` at `start`. */
|
|
76
|
-
readonly compose: ToneOfflineComposeFn;
|
|
77
|
-
/** Optional cooperative-cancellation signal — see module doc above. */
|
|
78
|
-
readonly signal?: AbortSignal;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface ToneOfflineRenderResult {
|
|
82
|
-
/** The rendered PCM, as a real (native) `AudioBuffer` — `.getChannelData(n)`
|
|
83
|
-
* gives the Float32 samples for channel `n`, ready for a digest/RMS
|
|
84
|
-
* computation or (eventually, G4/I6) FFmpeg muxing. */
|
|
85
|
-
readonly buffer: AudioBuffer;
|
|
86
|
-
readonly durationSeconds: number;
|
|
87
|
-
readonly sampleRate: number;
|
|
88
|
-
readonly channels: number;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const DEFAULT_SAMPLE_RATE = 44100;
|
|
92
|
-
const DEFAULT_CHANNELS = 2;
|
|
93
|
-
|
|
94
|
-
/** Render `request.compose`'s Tone graph offline over `[start, end)`,
|
|
95
|
-
* producing a deterministic `AudioBuffer`. See module doc for the
|
|
96
|
-
* determinism/cancellation contract. */
|
|
97
|
-
export async function renderToneOffline(
|
|
98
|
-
request: ToneOfflineRenderRequest,
|
|
99
|
-
): Promise<ToneOfflineRenderResult> {
|
|
100
|
-
const {
|
|
101
|
-
start,
|
|
102
|
-
end,
|
|
103
|
-
sampleRate = DEFAULT_SAMPLE_RATE,
|
|
104
|
-
channels = DEFAULT_CHANNELS,
|
|
105
|
-
compose,
|
|
106
|
-
signal,
|
|
107
|
-
} = request;
|
|
108
|
-
|
|
109
|
-
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) {
|
|
110
|
-
throw new Error(
|
|
111
|
-
`[tone-offline-render] invalid range [${start}, ${end}) — end must be a finite number > start`,
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
if (!Number.isFinite(sampleRate) || sampleRate <= 0) {
|
|
115
|
-
throw new Error(
|
|
116
|
-
`[tone-offline-render] sampleRate must be a finite number > 0, got ${sampleRate}`,
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
if (!Number.isInteger(channels) || channels <= 0) {
|
|
120
|
-
throw new Error(`[tone-offline-render] channels must be a positive integer, got ${channels}`);
|
|
121
|
-
}
|
|
122
|
-
// Cooperative cancellation, checked BEFORE ever constructing an
|
|
123
|
-
// OfflineAudioContext — an already-aborted signal must never start a
|
|
124
|
-
// render at all (see module doc).
|
|
125
|
-
if (signal?.aborted) {
|
|
126
|
-
throw createAbortError();
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const durationSeconds = end - start;
|
|
130
|
-
const renderPromise = Offline(compose, durationSeconds, channels, sampleRate);
|
|
131
|
-
const toneBuffer = signal ? await raceAbort(renderPromise, signal) : await renderPromise;
|
|
132
|
-
const buffer = toneBuffer.get();
|
|
133
|
-
if (!buffer) {
|
|
134
|
-
throw new Error('[tone-offline-render] Tone.Offline resolved without a decoded AudioBuffer');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
buffer,
|
|
139
|
-
durationSeconds,
|
|
140
|
-
sampleRate: buffer.sampleRate,
|
|
141
|
-
channels: buffer.numberOfChannels,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function createAbortError(): DOMException {
|
|
146
|
-
return new DOMException('[tone-offline-render] render canceled', 'AbortError');
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Race `promise` against `signal` firing — resolves/rejects with whichever
|
|
150
|
-
* happens first, without altering the underlying Web Audio render (see
|
|
151
|
-
* module doc's "cooperative, not a true mid-render abort" section). */
|
|
152
|
-
function raceAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
153
|
-
return new Promise<T>((resolve, reject) => {
|
|
154
|
-
const onAbort = () => reject(createAbortError());
|
|
155
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
156
|
-
promise.then(
|
|
157
|
-
(value) => {
|
|
158
|
-
signal.removeEventListener('abort', onAbort);
|
|
159
|
-
resolve(value);
|
|
160
|
-
},
|
|
161
|
-
(err: unknown) => {
|
|
162
|
-
signal.removeEventListener('abort', onAbort);
|
|
163
|
-
reject(err);
|
|
164
|
-
},
|
|
165
|
-
);
|
|
166
|
-
});
|
|
167
|
-
}
|