@vgai/engine 0.2.0 → 0.4.0-canary.20260715.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/package.json +24 -4
- package/schemas/engine-api.json +124 -0
- package/schemas/engine-api.md +53 -0
- package/schemas/engine-capabilities.json +124 -0
- package/schemas/inputmap.schema.json +314 -0
- package/schemas/mat.schema.json +286 -0
- package/schemas/prefab.schema.json +10148 -0
- package/schemas/scn2d.schema.json +475 -0
- package/schemas/vgai-game.schema.json +383 -0
- package/schemas/vscn.schema.json +11007 -0
- package/src/adapter/{world-kind.ts → adapter-surface.ts} +6 -6
- package/src/adapter/authoring.ts +77 -0
- package/src/adapter/first-party-systems.ts +23 -34
- package/src/adapter/game-adapter.ts +8 -8
- package/src/adapter/host-context.ts +2 -4
- package/src/adapter/index.ts +4 -4
- package/src/adapter/system-adapter.ts +88 -22
- package/src/adapter/vgai-scene-game-adapter.ts +244 -194
- package/src/animation/anim-graph-types.ts +12 -43
- package/src/animation/animation-clock.ts +479 -0
- package/src/animation/camera-ownership.ts +467 -0
- package/src/animation/cinematic-cues.ts +451 -0
- package/src/animation/clip-map.ts +41 -0
- package/src/animation/gsap-registration.ts +184 -0
- package/src/animation/theatre-clock-binding.ts +111 -0
- package/src/animation/theatre-director.ts +347 -0
- package/src/animation/theatre-object-binding.ts +661 -0
- package/src/animation/xstate-animation-binding.ts +436 -0
- package/src/animation/xstate-animation-meta.ts +319 -0
- package/src/audio/index.ts +39 -7
- package/src/audio/tone-clock-binding.ts +98 -0
- package/src/audio/tone-context.ts +129 -0
- package/src/audio/tone-offline-render.ts +167 -0
- package/src/audio/wav-encode.ts +119 -0
- package/src/character/cloth-sim.ts +533 -0
- package/src/character/spring-chain.ts +307 -0
- package/src/core/game-loop.ts +57 -2
- package/src/core/seeded-random.ts +161 -0
- package/src/core/system-runner.ts +20 -3
- package/src/core/types.ts +50 -0
- package/src/data/data-asset.ts +167 -0
- package/src/data/data-check-core.ts +242 -0
- package/src/data/data-ref.ts +145 -0
- package/src/data/vite-plugin-data.ts +290 -0
- package/src/dev/performance-profiler.ts +213 -0
- package/src/dev/webgl-gpu-timer.ts +53 -0
- package/src/ecs/component-manager.ts +45 -12
- package/src/ecs/game-component.ts +95 -11
- package/src/humanoid/bake.operation.ts +326 -0
- package/src/humanoid/body.ts +663 -0
- package/src/humanoid/clips.ts +149 -0
- package/src/humanoid/compose.ts +209 -0
- package/src/humanoid/generate.ts +189 -0
- package/src/humanoid/index.ts +36 -0
- package/src/humanoid/schema.ts +108 -0
- package/src/humanoid/skeleton.ts +345 -0
- package/src/index.ts +48 -0
- package/src/input/input-manager.ts +1886 -33
- package/src/input/input-types.ts +158 -3
- package/src/input/prompt-labels.ts +122 -0
- package/src/input/rebind-controller.ts +105 -0
- package/src/input/schema.ts +206 -52
- package/src/manifest/index.ts +5 -5
- package/src/manifest/load.ts +125 -72
- package/src/manifest/schema.ts +362 -255
- package/src/react/game-state.tsx +135 -32
- package/src/react/root-adapter.tsx +49 -0
- package/src/react/unmanaged-root-detector.ts +66 -0
- package/src/react/use-data.ts +124 -0
- package/src/react/use-selection.tsx +135 -0
- package/src/runtime/create-runtime.ts +112 -273
- package/src/runtime/debug-bridge.ts +483 -0
- package/src/runtime/debug-registry.ts +856 -0
- package/src/runtime/game.ts +342 -93
- package/src/runtime/gameplay-rng-trap.ts +134 -0
- package/src/runtime/input-router.ts +7 -7
- package/src/runtime/mount-game.ts +40 -38
- package/src/runtime/mount-manifest.ts +169 -37
- package/src/runtime/render-audio-control.ts +168 -0
- package/src/runtime/render-control.ts +522 -0
- package/src/runtime/render-seed.ts +79 -0
- package/src/runtime/state-bridge.ts +24 -10
- package/src/runtime/types.ts +110 -33
- package/src/scene/asset-loaders.ts +10 -36
- package/src/scene/asset-paths.ts +0 -2
- package/src/scene/asset-ref-check.ts +248 -0
- package/src/scene/asset-registry.ts +22 -0
- package/src/scene/component-registry.ts +14 -3
- package/src/scene/defaults.ts +1 -0
- package/src/scene/light-camera-factory.ts +11 -3
- package/src/scene/parse.ts +133 -0
- package/src/scene/scene-apply.ts +55 -4
- package/src/scene/scene-loader.ts +91 -123
- package/src/scene/scene-types.ts +0 -1
- package/src/scene/schema/animation.ts +30 -79
- package/src/scene/schema/entity.ts +20 -0
- package/src/scene/schema/index.ts +2 -46
- package/src/scene/schema/light.ts +16 -1
- package/src/scene/schema/material.ts +96 -91
- package/src/scene/schema/scene-file.ts +1 -7
- package/src/scene/user-data.ts +22 -10
- package/src/setup/setup-renderer.ts +10 -3
- package/src/tools/define-tool.ts +191 -0
- package/src/world2d/authoring-2d.ts +17 -1
- package/src/world2d/collision-2d.ts +1 -1
- package/src/world2d/pixi-game-adapter.ts +19 -17
- package/src/world2d/scene2d-loader.ts +1 -0
- package/src/world2d/types.ts +8 -2
- package/src/animation/anim-graph.ts +0 -406
- package/src/animation/anim-system.ts +0 -28
- package/src/animation/property-track.ts +0 -178
- package/src/animation/schema.ts +0 -204
- package/src/audio/ambient.ts +0 -300
- package/src/audio/impacts.ts +0 -212
- package/src/audio/movement.ts +0 -140
- package/src/audio/musical.ts +0 -200
- package/src/audio/ui-sounds.ts +0 -171
- package/src/audio/vehicle.ts +0 -235
- package/src/audio/weapons.ts +0 -152
- package/src/runtime/scene-ui-bridge.ts +0 -86
- package/src/runtime/scene-ui-data.ts +0 -119
- package/src/scene/schema/ui.ts +0 -602
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure PCM -> 16-bit WAV encoder (spec §15 I6 "Encode and Mux", audio half —
|
|
3
|
+
* `docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md`). This module takes
|
|
4
|
+
* already-rendered Float32 channel data (e.g. `AudioBuffer.getChannelData(n)`
|
|
5
|
+
* from a `Tone.Offline` render, `audio/tone-offline-render.ts`) and produces
|
|
6
|
+
* a standard little-endian, 16-bit-PCM, interleaved RIFF/WAVE file as raw
|
|
7
|
+
* bytes — no `AudioBuffer`/`AudioContext` dependency, so this runs equally in
|
|
8
|
+
* a browser render-mode page (`runtime/render-audio-control.ts`) and in
|
|
9
|
+
* plain Node (this module's own `packages/engine/test/audio/wav-encode.test.ts`,
|
|
10
|
+
* no Playwright needed).
|
|
11
|
+
*
|
|
12
|
+
* 16-bit PCM (not 32-bit float) is the deliberate choice named by the I6 AC
|
|
13
|
+
* ("16-bit PCM WAV is fine") — it is universally decodable by every FFmpeg
|
|
14
|
+
* build without an extra format flag, keeping the video-mux step's ffmpeg
|
|
15
|
+
* invocation ordinary and portable.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface PcmToWavInput {
|
|
19
|
+
/** One `Float32Array` per channel, each sample in `[-1, 1]` (values outside
|
|
20
|
+
* that range are clamped, not thrown on — real synthesis can transiently
|
|
21
|
+
* clip). All channels must have equal length. */
|
|
22
|
+
readonly channelData: readonly Float32Array[];
|
|
23
|
+
readonly sampleRate: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const BYTES_PER_SAMPLE = 2; // 16-bit PCM
|
|
27
|
+
const WAV_HEADER_BYTES = 44;
|
|
28
|
+
|
|
29
|
+
function clamp(value: number, lo: number, hi: number): number {
|
|
30
|
+
return Math.min(hi, Math.max(lo, value));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeAsciiString(view: DataView, offset: number, text: string): void {
|
|
34
|
+
for (let i = 0; i < text.length; i++) {
|
|
35
|
+
view.setUint8(offset + i, text.charCodeAt(i));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Encode `input` as a complete little-endian, 16-bit-PCM, interleaved
|
|
40
|
+
* RIFF/WAVE file (44-byte canonical header + data chunk). Throws on an
|
|
41
|
+
* empty/mismatched channel set rather than silently producing a malformed
|
|
42
|
+
* file — this module's own contribution to the "fail loudly, never a
|
|
43
|
+
* silent no-op" convention `runtime/render-control.ts` documents. */
|
|
44
|
+
export function encodeWav16(input: PcmToWavInput): Uint8Array {
|
|
45
|
+
const { channelData, sampleRate } = input;
|
|
46
|
+
const numChannels = channelData.length;
|
|
47
|
+
if (numChannels === 0) {
|
|
48
|
+
throw new Error('[wav-encode] channelData must contain at least one channel');
|
|
49
|
+
}
|
|
50
|
+
if (!Number.isFinite(sampleRate) || sampleRate <= 0) {
|
|
51
|
+
throw new Error(`[wav-encode] sampleRate must be a finite number > 0, got ${sampleRate}`);
|
|
52
|
+
}
|
|
53
|
+
const numFrames = channelData[0]?.length ?? 0;
|
|
54
|
+
for (let ch = 0; ch < numChannels; ch++) {
|
|
55
|
+
const len = channelData[ch]?.length ?? -1;
|
|
56
|
+
if (len !== numFrames) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`[wav-encode] channel ${ch} has length ${len}, expected ${numFrames} (all channels must match)`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const blockAlign = numChannels * BYTES_PER_SAMPLE;
|
|
64
|
+
const byteRate = sampleRate * blockAlign;
|
|
65
|
+
const dataSize = numFrames * blockAlign;
|
|
66
|
+
|
|
67
|
+
const buffer = new ArrayBuffer(WAV_HEADER_BYTES + dataSize);
|
|
68
|
+
const view = new DataView(buffer);
|
|
69
|
+
|
|
70
|
+
// RIFF chunk descriptor
|
|
71
|
+
writeAsciiString(view, 0, 'RIFF');
|
|
72
|
+
view.setUint32(4, 36 + dataSize, true);
|
|
73
|
+
writeAsciiString(view, 8, 'WAVE');
|
|
74
|
+
|
|
75
|
+
// fmt subchunk (PCM)
|
|
76
|
+
writeAsciiString(view, 12, 'fmt ');
|
|
77
|
+
view.setUint32(16, 16, true); // subchunk size for PCM
|
|
78
|
+
view.setUint16(20, 1, true); // audio format 1 = PCM
|
|
79
|
+
view.setUint16(22, numChannels, true);
|
|
80
|
+
view.setUint32(24, sampleRate, true);
|
|
81
|
+
view.setUint32(28, byteRate, true);
|
|
82
|
+
view.setUint16(32, blockAlign, true);
|
|
83
|
+
view.setUint16(34, BYTES_PER_SAMPLE * 8, true); // bits per sample
|
|
84
|
+
|
|
85
|
+
// data subchunk
|
|
86
|
+
writeAsciiString(view, 36, 'data');
|
|
87
|
+
view.setUint32(40, dataSize, true);
|
|
88
|
+
|
|
89
|
+
let offset = WAV_HEADER_BYTES;
|
|
90
|
+
for (let i = 0; i < numFrames; i++) {
|
|
91
|
+
for (let ch = 0; ch < numChannels; ch++) {
|
|
92
|
+
const sample = clamp(channelData[ch]?.[i] ?? 0, -1, 1);
|
|
93
|
+
const intSample = Math.round(sample < 0 ? sample * 0x8000 : sample * 0x7fff);
|
|
94
|
+
view.setInt16(offset, intSample, true);
|
|
95
|
+
offset += BYTES_PER_SAMPLE;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return new Uint8Array(buffer);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** RMS + peak over all channels/samples — the same "non-trivial audio, not
|
|
103
|
+
* an all-zero buffer" sanity check `tone-audio-main.ts`'s fixture computes,
|
|
104
|
+
* factored out here so both that fixture and the render-audio harness can
|
|
105
|
+
* share one implementation instead of drifting copies. */
|
|
106
|
+
export function pcmStats(channelData: readonly Float32Array[]): { rms: number; peak: number } {
|
|
107
|
+
let sumSquares = 0;
|
|
108
|
+
let peak = 0;
|
|
109
|
+
let count = 0;
|
|
110
|
+
for (const data of channelData) {
|
|
111
|
+
for (let i = 0; i < data.length; i++) {
|
|
112
|
+
const v = data[i] ?? 0;
|
|
113
|
+
sumSquares += v * v;
|
|
114
|
+
peak = Math.max(peak, Math.abs(v));
|
|
115
|
+
count++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { rms: count > 0 ? Math.sqrt(sumSquares / count) : 0, peak };
|
|
119
|
+
}
|