@vgai/engine 0.2.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/LICENSE +202 -0
- package/README.md +35 -0
- package/package.json +55 -0
- package/src/adapter/authoring.ts +402 -0
- package/src/adapter/colyseus-networking-adapter.ts +72 -0
- package/src/adapter/first-party-systems.ts +103 -0
- package/src/adapter/game-adapter.ts +151 -0
- package/src/adapter/host-context.ts +77 -0
- package/src/adapter/index.ts +85 -0
- package/src/adapter/ingest/game-contract.ts +59 -0
- package/src/adapter/ingest/overlay-applier.ts +207 -0
- package/src/adapter/ingest/overlay-apply.ts +124 -0
- package/src/adapter/ingest/overlay-file.ts +126 -0
- package/src/adapter/ingest/overlay-report.ts +176 -0
- package/src/adapter/ingest/scene-capture.ts +307 -0
- package/src/adapter/ingest/upstream-pin.ts +52 -0
- package/src/adapter/loop-gate-report.ts +54 -0
- package/src/adapter/rapier-physics-adapter.ts +56 -0
- package/src/adapter/system-adapter.ts +154 -0
- package/src/adapter/transform.ts +18 -0
- package/src/adapter/vgai-scene-game-adapter.ts +886 -0
- package/src/adapter/world-kind.ts +34 -0
- package/src/ai/navigation.ts +164 -0
- package/src/animation/anim-graph-types.ts +56 -0
- package/src/animation/anim-graph.ts +406 -0
- package/src/animation/anim-system.ts +28 -0
- package/src/animation/blend-node.ts +119 -0
- package/src/animation/property-track.ts +178 -0
- package/src/animation/schema.ts +204 -0
- package/src/assets.ts +80 -0
- package/src/audio/ambient.ts +300 -0
- package/src/audio/impacts.ts +212 -0
- package/src/audio/index.ts +7 -0
- package/src/audio/movement.ts +140 -0
- package/src/audio/musical.ts +200 -0
- package/src/audio/ui-sounds.ts +171 -0
- package/src/audio/vehicle.ts +235 -0
- package/src/audio/weapons.ts +152 -0
- package/src/core/game-loop.ts +127 -0
- package/src/core/system-runner.ts +298 -0
- package/src/core/types.ts +58 -0
- package/src/dev/console-bridge.ts +83 -0
- package/src/dev/debug-draw.ts +80 -0
- package/src/dev/logger.ts +119 -0
- package/src/ecs/component-manager.ts +748 -0
- package/src/ecs/game-component.ts +147 -0
- package/src/ecs/hmr-swap-report.ts +65 -0
- package/src/input/input-manager.ts +439 -0
- package/src/input/input-types.ts +19 -0
- package/src/input/schema.ts +129 -0
- package/src/loader.ts +70 -0
- package/src/manifest/index.ts +24 -0
- package/src/manifest/load-file.ts +16 -0
- package/src/manifest/load.ts +378 -0
- package/src/manifest/schema.ts +375 -0
- package/src/physics/collision-system.ts +76 -0
- package/src/physics/physics-registry.ts +83 -0
- package/src/physics/transform-writer.ts +41 -0
- package/src/physics/trigger-dispatch.ts +97 -0
- package/src/react/game-state.tsx +172 -0
- package/src/render/auto-batcher.ts +169 -0
- package/src/render/render-batch-system.ts +268 -0
- package/src/render/render-features.ts +146 -0
- package/src/render/render-settings.ts +72 -0
- package/src/runtime/create-runtime.ts +1152 -0
- package/src/runtime/frame-selector-cache.ts +81 -0
- package/src/runtime/game.ts +1003 -0
- package/src/runtime/input-router.ts +213 -0
- package/src/runtime/mount-game.ts +269 -0
- package/src/runtime/mount-manifest.ts +361 -0
- package/src/runtime/scene-ui-bridge.ts +86 -0
- package/src/runtime/scene-ui-data.ts +119 -0
- package/src/runtime/state-bridge.ts +79 -0
- package/src/runtime/types.ts +196 -0
- package/src/scene/asset-loaders.ts +195 -0
- package/src/scene/asset-paths.ts +123 -0
- package/src/scene/asset-registry.ts +67 -0
- package/src/scene/collider-dimensions.ts +125 -0
- package/src/scene/component-registry.ts +40 -0
- package/src/scene/defaults.ts +164 -0
- package/src/scene/geometries/index.ts +7 -0
- package/src/scene/geometries/terrain.ts +42 -0
- package/src/scene/geometry-registry.ts +42 -0
- package/src/scene/instance-registry.ts +84 -0
- package/src/scene/instancers/grid.ts +38 -0
- package/src/scene/instancers/index.ts +7 -0
- package/src/scene/light-camera-factory.ts +97 -0
- package/src/scene/material-factory.ts +211 -0
- package/src/scene/material-registry.ts +73 -0
- package/src/scene/materials/index.ts +7 -0
- package/src/scene/materials/water.ts +56 -0
- package/src/scene/parse.ts +71 -0
- package/src/scene/particles-factory.ts +383 -0
- package/src/scene/scene-apply.ts +356 -0
- package/src/scene/scene-diff-schema.ts +115 -0
- package/src/scene/scene-diff-types.ts +29 -0
- package/src/scene/scene-loader.ts +1533 -0
- package/src/scene/scene-query.ts +63 -0
- package/src/scene/scene-types.ts +34 -0
- package/src/scene/scene-version.ts +40 -0
- package/src/scene/schema/animation.ts +95 -0
- package/src/scene/schema/audio.ts +25 -0
- package/src/scene/schema/camera.ts +21 -0
- package/src/scene/schema/collider.ts +69 -0
- package/src/scene/schema/entity-ref.ts +78 -0
- package/src/scene/schema/entity.ts +169 -0
- package/src/scene/schema/environment.ts +384 -0
- package/src/scene/schema/index.ts +95 -0
- package/src/scene/schema/instances.ts +35 -0
- package/src/scene/schema/joint.ts +26 -0
- package/src/scene/schema/light.ts +38 -0
- package/src/scene/schema/material.ts +113 -0
- package/src/scene/schema/mesh.ts +108 -0
- package/src/scene/schema/particles.ts +398 -0
- package/src/scene/schema/physics.ts +49 -0
- package/src/scene/schema/scene-file.ts +299 -0
- package/src/scene/schema/shadow.ts +24 -0
- package/src/scene/schema/spline.ts +21 -0
- package/src/scene/schema/tuples.ts +21 -0
- package/src/scene/schema/ui.ts +602 -0
- package/src/scene/user-data.ts +203 -0
- package/src/setup/setup-audio.ts +60 -0
- package/src/setup/setup-particles.ts +23 -0
- package/src/setup/setup-physics.ts +67 -0
- package/src/setup/setup-renderer.ts +529 -0
- package/src/types-n8ao.d.ts +37 -0
- package/src/types-realism-effects.d.ts +61 -0
- package/src/world2d/authoring-2d.ts +208 -0
- package/src/world2d/capture-to-scene2d.ts +52 -0
- package/src/world2d/collision-2d.ts +106 -0
- package/src/world2d/components-2d.ts +86 -0
- package/src/world2d/index.ts +66 -0
- package/src/world2d/ingest-iframe-2d.ts +255 -0
- package/src/world2d/ingest2d.ts +131 -0
- package/src/world2d/physics2d-registry.ts +49 -0
- package/src/world2d/pixi-game-adapter.ts +325 -0
- package/src/world2d/pixi-surface.ts +78 -0
- package/src/world2d/scene-capture-2d.ts +117 -0
- package/src/world2d/scene2d-loader.ts +308 -0
- package/src/world2d/schema/entity2d.ts +145 -0
- package/src/world2d/schema/physics2d.ts +53 -0
- package/src/world2d/schema/sprite.ts +71 -0
- package/src/world2d/schema/tilemap.ts +22 -0
- package/src/world2d/schema/tuples2d.ts +25 -0
- package/src/world2d/system-adapters-2d.ts +49 -0
- package/src/world2d/transform-writer-2d.ts +24 -0
- package/src/world2d/types.ts +55 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Musical sounds — rhythmic and melodic elements.
|
|
3
|
+
*
|
|
4
|
+
* bassPulse and arpeggio are continuous (play/stop).
|
|
5
|
+
* stinger is one-shot.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Bass pulse — rhythmic low sine following a beat pattern. */
|
|
9
|
+
export function bassPulse(
|
|
10
|
+
ctx: AudioContext,
|
|
11
|
+
dest: AudioNode,
|
|
12
|
+
options?: { frequency?: number; bpm?: number; volume?: number; pattern?: number[] },
|
|
13
|
+
) {
|
|
14
|
+
const frequency = options?.frequency ?? 55;
|
|
15
|
+
const bpm = options?.bpm ?? 120;
|
|
16
|
+
const volume = options?.volume ?? 0.3;
|
|
17
|
+
const pattern = options?.pattern ?? [1, 0, 1, 0];
|
|
18
|
+
const beatInterval = 60 / bpm;
|
|
19
|
+
|
|
20
|
+
const out = ctx.createGain();
|
|
21
|
+
out.gain.value = volume;
|
|
22
|
+
out.connect(dest);
|
|
23
|
+
|
|
24
|
+
let osc: OscillatorNode | null = null;
|
|
25
|
+
let pulseGain: GainNode | null = null;
|
|
26
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
27
|
+
let step = 0;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
play() {
|
|
31
|
+
if (osc) return;
|
|
32
|
+
osc = ctx.createOscillator();
|
|
33
|
+
osc.type = 'sine';
|
|
34
|
+
osc.frequency.value = frequency;
|
|
35
|
+
pulseGain = ctx.createGain();
|
|
36
|
+
pulseGain.gain.value = 0;
|
|
37
|
+
osc.connect(pulseGain);
|
|
38
|
+
pulseGain.connect(out);
|
|
39
|
+
osc.start();
|
|
40
|
+
step = 0;
|
|
41
|
+
|
|
42
|
+
const tick = () => {
|
|
43
|
+
if (!pulseGain) return;
|
|
44
|
+
const beat = pattern[step % pattern.length]!;
|
|
45
|
+
const now = ctx.currentTime;
|
|
46
|
+
if (beat) {
|
|
47
|
+
pulseGain.gain.setValueAtTime(1, now);
|
|
48
|
+
pulseGain.gain.exponentialRampToValueAtTime(0.01, now + beatInterval * 0.8);
|
|
49
|
+
}
|
|
50
|
+
step++;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
tick();
|
|
54
|
+
timer = setInterval(tick, beatInterval * 1000);
|
|
55
|
+
},
|
|
56
|
+
stop() {
|
|
57
|
+
if (timer) {
|
|
58
|
+
clearInterval(timer);
|
|
59
|
+
timer = null;
|
|
60
|
+
}
|
|
61
|
+
if (!osc) return;
|
|
62
|
+
const refs = { osc, pulseGain };
|
|
63
|
+
osc = null;
|
|
64
|
+
pulseGain = null;
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
try {
|
|
67
|
+
refs.osc.stop();
|
|
68
|
+
} catch {
|
|
69
|
+
/* already stopped */
|
|
70
|
+
}
|
|
71
|
+
refs.osc.disconnect();
|
|
72
|
+
refs.pulseGain?.disconnect();
|
|
73
|
+
}, 100);
|
|
74
|
+
},
|
|
75
|
+
dispose() {
|
|
76
|
+
if (timer) {
|
|
77
|
+
clearInterval(timer);
|
|
78
|
+
timer = null;
|
|
79
|
+
}
|
|
80
|
+
if (osc) {
|
|
81
|
+
try {
|
|
82
|
+
osc.stop();
|
|
83
|
+
} catch {
|
|
84
|
+
/* already stopped */
|
|
85
|
+
}
|
|
86
|
+
osc.disconnect();
|
|
87
|
+
}
|
|
88
|
+
pulseGain?.disconnect();
|
|
89
|
+
osc = null;
|
|
90
|
+
pulseGain = null;
|
|
91
|
+
out.disconnect();
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Arpeggio — repeating note sequence. */
|
|
97
|
+
export function arpeggio(
|
|
98
|
+
ctx: AudioContext,
|
|
99
|
+
dest: AudioNode,
|
|
100
|
+
options?: {
|
|
101
|
+
notes?: number[];
|
|
102
|
+
bpm?: number;
|
|
103
|
+
waveform?: OscillatorType;
|
|
104
|
+
volume?: number;
|
|
105
|
+
},
|
|
106
|
+
) {
|
|
107
|
+
const notes = options?.notes ?? [262, 330, 392, 523];
|
|
108
|
+
const bpm = options?.bpm ?? 140;
|
|
109
|
+
const waveform = options?.waveform ?? 'triangle';
|
|
110
|
+
const volume = options?.volume ?? 0.2;
|
|
111
|
+
const noteInterval = 60 / bpm;
|
|
112
|
+
|
|
113
|
+
const out = ctx.createGain();
|
|
114
|
+
out.gain.value = volume;
|
|
115
|
+
out.connect(dest);
|
|
116
|
+
|
|
117
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
118
|
+
let step = 0;
|
|
119
|
+
|
|
120
|
+
function playNote(freq: number) {
|
|
121
|
+
const now = ctx.currentTime;
|
|
122
|
+
const osc = ctx.createOscillator();
|
|
123
|
+
osc.type = waveform;
|
|
124
|
+
osc.frequency.value = freq;
|
|
125
|
+
|
|
126
|
+
const env = ctx.createGain();
|
|
127
|
+
env.gain.setValueAtTime(0.8, now);
|
|
128
|
+
env.gain.exponentialRampToValueAtTime(0.001, now + noteInterval * 0.9);
|
|
129
|
+
|
|
130
|
+
osc.connect(env);
|
|
131
|
+
env.connect(out);
|
|
132
|
+
osc.start(now);
|
|
133
|
+
osc.stop(now + noteInterval);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
play() {
|
|
138
|
+
if (timer) return;
|
|
139
|
+
step = 0;
|
|
140
|
+
const tick = () => {
|
|
141
|
+
playNote(notes[step % notes.length]!);
|
|
142
|
+
step++;
|
|
143
|
+
};
|
|
144
|
+
tick();
|
|
145
|
+
timer = setInterval(tick, noteInterval * 1000);
|
|
146
|
+
},
|
|
147
|
+
stop() {
|
|
148
|
+
if (timer) {
|
|
149
|
+
clearInterval(timer);
|
|
150
|
+
timer = null;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
dispose() {
|
|
154
|
+
if (timer) {
|
|
155
|
+
clearInterval(timer);
|
|
156
|
+
timer = null;
|
|
157
|
+
}
|
|
158
|
+
out.disconnect();
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Stinger — one-shot ascending note sequence for events. */
|
|
164
|
+
export function stinger(
|
|
165
|
+
ctx: AudioContext,
|
|
166
|
+
dest: AudioNode,
|
|
167
|
+
options?: { notes?: number[]; volume?: number; tempo?: number },
|
|
168
|
+
) {
|
|
169
|
+
const notes = options?.notes ?? [392, 494, 587, 784];
|
|
170
|
+
const volume = options?.volume ?? 0.3;
|
|
171
|
+
const tempo = options?.tempo ?? 0.12;
|
|
172
|
+
|
|
173
|
+
const out = ctx.createGain();
|
|
174
|
+
out.gain.value = volume;
|
|
175
|
+
out.connect(dest);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
play() {
|
|
179
|
+
const now = ctx.currentTime;
|
|
180
|
+
for (let i = 0; i < notes.length; i++) {
|
|
181
|
+
const t = now + i * tempo;
|
|
182
|
+
const osc = ctx.createOscillator();
|
|
183
|
+
osc.type = 'triangle';
|
|
184
|
+
osc.frequency.value = notes[i]!;
|
|
185
|
+
|
|
186
|
+
const env = ctx.createGain();
|
|
187
|
+
env.gain.setValueAtTime(0.8, t);
|
|
188
|
+
env.gain.exponentialRampToValueAtTime(0.001, t + tempo * 1.5);
|
|
189
|
+
|
|
190
|
+
osc.connect(env);
|
|
191
|
+
env.connect(out);
|
|
192
|
+
osc.start(t);
|
|
193
|
+
osc.stop(t + tempo * 1.5 + 0.01);
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
dispose() {
|
|
197
|
+
out.disconnect();
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI sounds — short tonal feedback for interface interactions.
|
|
3
|
+
*
|
|
4
|
+
* All one-shot: call play() to trigger, dispose() to clean up.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Click — short sine pip. */
|
|
8
|
+
export function uiClick(
|
|
9
|
+
ctx: AudioContext,
|
|
10
|
+
dest: AudioNode,
|
|
11
|
+
options?: { pitch?: number; volume?: number },
|
|
12
|
+
) {
|
|
13
|
+
const pitch = options?.pitch ?? 800;
|
|
14
|
+
const volume = options?.volume ?? 0.2;
|
|
15
|
+
|
|
16
|
+
const out = ctx.createGain();
|
|
17
|
+
out.gain.value = volume;
|
|
18
|
+
out.connect(dest);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
play() {
|
|
22
|
+
const now = ctx.currentTime;
|
|
23
|
+
const osc = ctx.createOscillator();
|
|
24
|
+
osc.type = 'sine';
|
|
25
|
+
osc.frequency.value = pitch;
|
|
26
|
+
|
|
27
|
+
const env = ctx.createGain();
|
|
28
|
+
env.gain.setValueAtTime(1, now);
|
|
29
|
+
env.gain.exponentialRampToValueAtTime(0.001, now + 0.04);
|
|
30
|
+
|
|
31
|
+
osc.connect(env);
|
|
32
|
+
env.connect(out);
|
|
33
|
+
osc.start(now);
|
|
34
|
+
osc.stop(now + 0.05);
|
|
35
|
+
},
|
|
36
|
+
dispose() {
|
|
37
|
+
out.disconnect();
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Hover — softer, higher sine pip. */
|
|
43
|
+
export function uiHover(
|
|
44
|
+
ctx: AudioContext,
|
|
45
|
+
dest: AudioNode,
|
|
46
|
+
options?: { pitch?: number; volume?: number },
|
|
47
|
+
) {
|
|
48
|
+
const pitch = options?.pitch ?? 1200;
|
|
49
|
+
const volume = options?.volume ?? 0.1;
|
|
50
|
+
|
|
51
|
+
const out = ctx.createGain();
|
|
52
|
+
out.gain.value = volume;
|
|
53
|
+
out.connect(dest);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
play() {
|
|
57
|
+
const now = ctx.currentTime;
|
|
58
|
+
const osc = ctx.createOscillator();
|
|
59
|
+
osc.type = 'sine';
|
|
60
|
+
osc.frequency.value = pitch;
|
|
61
|
+
|
|
62
|
+
const env = ctx.createGain();
|
|
63
|
+
env.gain.setValueAtTime(1, now);
|
|
64
|
+
env.gain.exponentialRampToValueAtTime(0.001, now + 0.03);
|
|
65
|
+
|
|
66
|
+
osc.connect(env);
|
|
67
|
+
env.connect(out);
|
|
68
|
+
osc.start(now);
|
|
69
|
+
osc.stop(now + 0.04);
|
|
70
|
+
},
|
|
71
|
+
dispose() {
|
|
72
|
+
out.disconnect();
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Success — two ascending tones (major third interval). */
|
|
78
|
+
export function uiSuccess(
|
|
79
|
+
ctx: AudioContext,
|
|
80
|
+
dest: AudioNode,
|
|
81
|
+
options?: { pitch?: number; volume?: number },
|
|
82
|
+
) {
|
|
83
|
+
const pitch = options?.pitch ?? 500;
|
|
84
|
+
const volume = options?.volume ?? 0.3;
|
|
85
|
+
|
|
86
|
+
const out = ctx.createGain();
|
|
87
|
+
out.gain.value = volume;
|
|
88
|
+
out.connect(dest);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
play() {
|
|
92
|
+
const now = ctx.currentTime;
|
|
93
|
+
|
|
94
|
+
// First note
|
|
95
|
+
const osc1 = ctx.createOscillator();
|
|
96
|
+
osc1.type = 'sine';
|
|
97
|
+
osc1.frequency.value = pitch;
|
|
98
|
+
const env1 = ctx.createGain();
|
|
99
|
+
env1.gain.setValueAtTime(1, now);
|
|
100
|
+
env1.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
|
|
101
|
+
osc1.connect(env1);
|
|
102
|
+
env1.connect(out);
|
|
103
|
+
osc1.start(now);
|
|
104
|
+
osc1.stop(now + 0.16);
|
|
105
|
+
|
|
106
|
+
// Second note — major third above, delayed 0.1s
|
|
107
|
+
const osc2 = ctx.createOscillator();
|
|
108
|
+
osc2.type = 'sine';
|
|
109
|
+
osc2.frequency.value = pitch * (5 / 4);
|
|
110
|
+
const env2 = ctx.createGain();
|
|
111
|
+
env2.gain.setValueAtTime(0.001, now);
|
|
112
|
+
env2.gain.setValueAtTime(1, now + 0.1);
|
|
113
|
+
env2.gain.exponentialRampToValueAtTime(0.001, now + 0.25);
|
|
114
|
+
osc2.connect(env2);
|
|
115
|
+
env2.connect(out);
|
|
116
|
+
osc2.start(now);
|
|
117
|
+
osc2.stop(now + 0.26);
|
|
118
|
+
},
|
|
119
|
+
dispose() {
|
|
120
|
+
out.disconnect();
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Error — two descending tones (minor second). */
|
|
126
|
+
export function uiError(
|
|
127
|
+
ctx: AudioContext,
|
|
128
|
+
dest: AudioNode,
|
|
129
|
+
options?: { pitch?: number; volume?: number },
|
|
130
|
+
) {
|
|
131
|
+
const pitch = options?.pitch ?? 400;
|
|
132
|
+
const volume = options?.volume ?? 0.3;
|
|
133
|
+
|
|
134
|
+
const out = ctx.createGain();
|
|
135
|
+
out.gain.value = volume;
|
|
136
|
+
out.connect(dest);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
play() {
|
|
140
|
+
const now = ctx.currentTime;
|
|
141
|
+
|
|
142
|
+
// First note
|
|
143
|
+
const osc1 = ctx.createOscillator();
|
|
144
|
+
osc1.type = 'sine';
|
|
145
|
+
osc1.frequency.value = pitch;
|
|
146
|
+
const env1 = ctx.createGain();
|
|
147
|
+
env1.gain.setValueAtTime(1, now);
|
|
148
|
+
env1.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
|
|
149
|
+
osc1.connect(env1);
|
|
150
|
+
env1.connect(out);
|
|
151
|
+
osc1.start(now);
|
|
152
|
+
osc1.stop(now + 0.16);
|
|
153
|
+
|
|
154
|
+
// Second note — minor second below, delayed 0.1s
|
|
155
|
+
const osc2 = ctx.createOscillator();
|
|
156
|
+
osc2.type = 'sine';
|
|
157
|
+
osc2.frequency.value = pitch * 0.9;
|
|
158
|
+
const env2 = ctx.createGain();
|
|
159
|
+
env2.gain.setValueAtTime(0.001, now);
|
|
160
|
+
env2.gain.setValueAtTime(1, now + 0.1);
|
|
161
|
+
env2.gain.exponentialRampToValueAtTime(0.001, now + 0.25);
|
|
162
|
+
osc2.connect(env2);
|
|
163
|
+
env2.connect(out);
|
|
164
|
+
osc2.start(now);
|
|
165
|
+
osc2.stop(now + 0.26);
|
|
166
|
+
},
|
|
167
|
+
dispose() {
|
|
168
|
+
out.disconnect();
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vehicle sounds — continuous sounds with real-time parameter control.
|
|
3
|
+
*
|
|
4
|
+
* Each returns play/stop/setX/dispose. Parameters use setTargetAtTime for smooth ramping.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function createNoiseBuffer(ctx: AudioContext, duration = 2): AudioBuffer {
|
|
8
|
+
const length = ctx.sampleRate * duration;
|
|
9
|
+
const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
|
|
10
|
+
const data = buffer.getChannelData(0);
|
|
11
|
+
for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
|
|
12
|
+
return buffer;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Engine drone — sawtooth oscillator with throttle-driven pitch and volume. */
|
|
16
|
+
export function engineDrone(
|
|
17
|
+
ctx: AudioContext,
|
|
18
|
+
dest: AudioNode,
|
|
19
|
+
options?: { baseFreq?: number; volume?: number },
|
|
20
|
+
) {
|
|
21
|
+
const baseFreq = options?.baseFreq ?? 60;
|
|
22
|
+
const maxVol = options?.volume ?? 0.4;
|
|
23
|
+
|
|
24
|
+
const out = ctx.createGain();
|
|
25
|
+
out.gain.value = 0;
|
|
26
|
+
out.connect(dest);
|
|
27
|
+
|
|
28
|
+
// Subtle waveshaping for grit
|
|
29
|
+
const shaper = ctx.createWaveShaper();
|
|
30
|
+
const curve = new Float32Array(256);
|
|
31
|
+
for (let i = 0; i < 256; i++) {
|
|
32
|
+
const x = (i / 128 - 1) * 1.5;
|
|
33
|
+
curve[i] = Math.tanh(x);
|
|
34
|
+
}
|
|
35
|
+
shaper.curve = curve;
|
|
36
|
+
shaper.connect(out);
|
|
37
|
+
|
|
38
|
+
let osc: OscillatorNode | null = null;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
play() {
|
|
42
|
+
if (osc) return;
|
|
43
|
+
osc = ctx.createOscillator();
|
|
44
|
+
osc.type = 'sawtooth';
|
|
45
|
+
osc.frequency.value = baseFreq;
|
|
46
|
+
osc.connect(shaper);
|
|
47
|
+
osc.start();
|
|
48
|
+
out.gain.setTargetAtTime(0.1, ctx.currentTime, 0.05);
|
|
49
|
+
},
|
|
50
|
+
stop() {
|
|
51
|
+
if (!osc) return;
|
|
52
|
+
out.gain.setTargetAtTime(0, ctx.currentTime, 0.05);
|
|
53
|
+
const ref = osc;
|
|
54
|
+
osc = null;
|
|
55
|
+
setTimeout(() => {
|
|
56
|
+
try {
|
|
57
|
+
ref.stop();
|
|
58
|
+
} catch {
|
|
59
|
+
/* already stopped */
|
|
60
|
+
}
|
|
61
|
+
ref.disconnect();
|
|
62
|
+
}, 200);
|
|
63
|
+
},
|
|
64
|
+
/** Set throttle 0–1. Adjusts pitch (1x–3x base) and volume. */
|
|
65
|
+
setThrottle(t: number) {
|
|
66
|
+
if (!osc) return;
|
|
67
|
+
const clamped = Math.max(0, Math.min(1, t));
|
|
68
|
+
osc.frequency.setTargetAtTime(baseFreq + clamped * baseFreq * 2, ctx.currentTime, 0.05);
|
|
69
|
+
out.gain.setTargetAtTime(0.1 + clamped * maxVol, ctx.currentTime, 0.05);
|
|
70
|
+
},
|
|
71
|
+
dispose() {
|
|
72
|
+
if (osc) {
|
|
73
|
+
try {
|
|
74
|
+
osc.stop();
|
|
75
|
+
} catch {
|
|
76
|
+
/* already stopped */
|
|
77
|
+
}
|
|
78
|
+
osc.disconnect();
|
|
79
|
+
osc = null;
|
|
80
|
+
}
|
|
81
|
+
shaper.disconnect();
|
|
82
|
+
out.disconnect();
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Brake squeal — high-frequency oscillator with LFO tremolo. */
|
|
88
|
+
export function brakeSqueal(
|
|
89
|
+
ctx: AudioContext,
|
|
90
|
+
dest: AudioNode,
|
|
91
|
+
options?: { pitch?: number; volume?: number },
|
|
92
|
+
) {
|
|
93
|
+
const pitch = options?.pitch ?? 2000;
|
|
94
|
+
const maxVol = options?.volume ?? 0.3;
|
|
95
|
+
|
|
96
|
+
const out = ctx.createGain();
|
|
97
|
+
out.gain.value = 0;
|
|
98
|
+
out.connect(dest);
|
|
99
|
+
|
|
100
|
+
let osc: OscillatorNode | null = null;
|
|
101
|
+
let lfo: OscillatorNode | null = null;
|
|
102
|
+
let lfoGain: GainNode | null = null;
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
play() {
|
|
106
|
+
if (osc) return;
|
|
107
|
+
osc = ctx.createOscillator();
|
|
108
|
+
osc.type = 'sine';
|
|
109
|
+
osc.frequency.value = pitch;
|
|
110
|
+
|
|
111
|
+
// LFO for tremolo effect
|
|
112
|
+
lfo = ctx.createOscillator();
|
|
113
|
+
lfo.type = 'sine';
|
|
114
|
+
lfo.frequency.value = 30;
|
|
115
|
+
lfoGain = ctx.createGain();
|
|
116
|
+
lfoGain.gain.value = 0.3;
|
|
117
|
+
lfo.connect(lfoGain);
|
|
118
|
+
lfoGain.connect(out.gain);
|
|
119
|
+
|
|
120
|
+
osc.connect(out);
|
|
121
|
+
osc.start();
|
|
122
|
+
lfo.start();
|
|
123
|
+
},
|
|
124
|
+
stop() {
|
|
125
|
+
if (!osc) return;
|
|
126
|
+
out.gain.setTargetAtTime(0, ctx.currentTime, 0.02);
|
|
127
|
+
const refs = { osc, lfo, lfoGain };
|
|
128
|
+
osc = null;
|
|
129
|
+
lfo = null;
|
|
130
|
+
lfoGain = null;
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
try {
|
|
133
|
+
refs.osc.stop();
|
|
134
|
+
} catch {
|
|
135
|
+
/* already stopped */
|
|
136
|
+
}
|
|
137
|
+
refs.osc.disconnect();
|
|
138
|
+
try {
|
|
139
|
+
refs.lfo?.stop();
|
|
140
|
+
} catch {
|
|
141
|
+
/* already stopped */
|
|
142
|
+
}
|
|
143
|
+
refs.lfo?.disconnect();
|
|
144
|
+
refs.lfoGain?.disconnect();
|
|
145
|
+
}, 100);
|
|
146
|
+
},
|
|
147
|
+
/** Set brake intensity 0–1. */
|
|
148
|
+
setIntensity(i: number) {
|
|
149
|
+
out.gain.setTargetAtTime(Math.max(0, Math.min(1, i)) * maxVol, ctx.currentTime, 0.02);
|
|
150
|
+
},
|
|
151
|
+
dispose() {
|
|
152
|
+
if (osc) {
|
|
153
|
+
try {
|
|
154
|
+
osc.stop();
|
|
155
|
+
} catch {
|
|
156
|
+
/* already stopped */
|
|
157
|
+
}
|
|
158
|
+
osc.disconnect();
|
|
159
|
+
}
|
|
160
|
+
if (lfo) {
|
|
161
|
+
try {
|
|
162
|
+
lfo.stop();
|
|
163
|
+
} catch {
|
|
164
|
+
/* already stopped */
|
|
165
|
+
}
|
|
166
|
+
lfo.disconnect();
|
|
167
|
+
}
|
|
168
|
+
lfoGain?.disconnect();
|
|
169
|
+
osc = null;
|
|
170
|
+
lfo = null;
|
|
171
|
+
lfoGain = null;
|
|
172
|
+
out.disconnect();
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Tire skid — filtered looping noise with intensity control. */
|
|
178
|
+
export function tireSkid(ctx: AudioContext, dest: AudioNode, options?: { volume?: number }) {
|
|
179
|
+
const maxVol = options?.volume ?? 0.3;
|
|
180
|
+
const noiseBuffer = createNoiseBuffer(ctx, 2);
|
|
181
|
+
|
|
182
|
+
const out = ctx.createGain();
|
|
183
|
+
out.gain.value = 0;
|
|
184
|
+
out.connect(dest);
|
|
185
|
+
|
|
186
|
+
const filter = ctx.createBiquadFilter();
|
|
187
|
+
filter.type = 'bandpass';
|
|
188
|
+
filter.frequency.value = 800;
|
|
189
|
+
filter.Q.value = 1.5;
|
|
190
|
+
filter.connect(out);
|
|
191
|
+
|
|
192
|
+
let src: AudioBufferSourceNode | null = null;
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
play() {
|
|
196
|
+
if (src) return;
|
|
197
|
+
src = ctx.createBufferSource();
|
|
198
|
+
src.buffer = noiseBuffer;
|
|
199
|
+
src.loop = true;
|
|
200
|
+
src.connect(filter);
|
|
201
|
+
src.start();
|
|
202
|
+
},
|
|
203
|
+
stop() {
|
|
204
|
+
if (!src) return;
|
|
205
|
+
out.gain.setTargetAtTime(0, ctx.currentTime, 0.02);
|
|
206
|
+
const ref = src;
|
|
207
|
+
src = null;
|
|
208
|
+
setTimeout(() => {
|
|
209
|
+
try {
|
|
210
|
+
ref.stop();
|
|
211
|
+
} catch {
|
|
212
|
+
/* already stopped */
|
|
213
|
+
}
|
|
214
|
+
ref.disconnect();
|
|
215
|
+
}, 100);
|
|
216
|
+
},
|
|
217
|
+
/** Set skid intensity 0–1. */
|
|
218
|
+
setIntensity(i: number) {
|
|
219
|
+
out.gain.setTargetAtTime(Math.max(0, Math.min(1, i)) * maxVol, ctx.currentTime, 0.02);
|
|
220
|
+
},
|
|
221
|
+
dispose() {
|
|
222
|
+
if (src) {
|
|
223
|
+
try {
|
|
224
|
+
src.stop();
|
|
225
|
+
} catch {
|
|
226
|
+
/* already stopped */
|
|
227
|
+
}
|
|
228
|
+
src.disconnect();
|
|
229
|
+
}
|
|
230
|
+
src = null;
|
|
231
|
+
filter.disconnect();
|
|
232
|
+
out.disconnect();
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|