@genex-ai/cli-demo 0.98.0-dev.284 → 0.99.0-dev.286
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/dist/index.js +24 -1
- package/package.json +1 -1
- package/templates/skills/genex-ai-music/SKILL.md +10 -3
- package/templates/skills/genex-ai-sfx/SKILL.md +105 -3
- package/templates/skills/genex-threejs-adaptive-quality/SKILL.md +25 -0
- package/templates/skills/genex-threejs-vehicle-controllers/references/drone.md +6 -0
package/dist/index.js
CHANGED
|
@@ -4908,7 +4908,12 @@ function splitArgs(text) {
|
|
|
4908
4908
|
var lineOf = (content, index) => content.slice(0, index).split("\n").length;
|
|
4909
4909
|
var DEPTH_RATIO_LIMIT = 1e6;
|
|
4910
4910
|
async function detectSurfaceScan(cwd = process.cwd()) {
|
|
4911
|
-
const found = {
|
|
4911
|
+
const found = {
|
|
4912
|
+
guessedRepeat: [],
|
|
4913
|
+
squarePoints: [],
|
|
4914
|
+
depthRange: [],
|
|
4915
|
+
mediaElementAudio: []
|
|
4916
|
+
};
|
|
4912
4917
|
const srcDir = path15.join(cwd, "src");
|
|
4913
4918
|
let entries;
|
|
4914
4919
|
try {
|
|
@@ -4955,6 +4960,18 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
4955
4960
|
ratio
|
|
4956
4961
|
});
|
|
4957
4962
|
}
|
|
4963
|
+
const threeAudioImport = /import\s+(?:type\s+)?\{[^}]*\bAudio\b[^}]*\}\s+from\s*["']three["']/.test(
|
|
4964
|
+
content
|
|
4965
|
+
);
|
|
4966
|
+
const mediaRe = /\bnew\s+Audio\s*\(|\bdocument\s*\.\s*createElement\s*\(\s*["'`]audio["'`]\s*\)/g;
|
|
4967
|
+
while (m = mediaRe.exec(content)) {
|
|
4968
|
+
const isCtor = m[0].startsWith("new");
|
|
4969
|
+
if (isCtor && threeAudioImport) continue;
|
|
4970
|
+
found.mediaElementAudio.push({
|
|
4971
|
+
where: `${rel}:${lineOf(content, m.index)}`,
|
|
4972
|
+
detail: isCtor ? "new Audio(...)" : 'document.createElement("audio")'
|
|
4973
|
+
});
|
|
4974
|
+
}
|
|
4958
4975
|
}
|
|
4959
4976
|
return found;
|
|
4960
4977
|
}
|
|
@@ -5079,6 +5096,12 @@ function surfaceNudges(log, s) {
|
|
|
5079
5096
|
`Particles will render as hard SQUARES: ${list} builds a PointsMaterial with no map/alphaMap, so every point is an opaque camera-facing quad. Give it a soft sprite (genex image --transparent) or a round gl_PointCoord cutout \u2014 and check the particle belongs there at all: genex-threejs-procedural-vfx decides that per moment.`
|
|
5080
5097
|
);
|
|
5081
5098
|
}
|
|
5099
|
+
if (s.mediaElementAudio.length) {
|
|
5100
|
+
const list = s.mediaElementAudio.map((d) => `${d.where} \u2014 ${d.detail}`).join("; ");
|
|
5101
|
+
log.warn(
|
|
5102
|
+
`HTML <audio> elements are carrying this game's sound: ${list}. On iPhone an <audio> element is a handle on the operating system's media pipeline in ANOTHER PROCESS: every write to .volume or .playbackRate is a cross-process call, and play() is slow and asynchronous. A sound driven from a live value \u2014 wheels from speed, engine from RPM \u2014 writes 60-120 of those a second and stutters the whole game, while one-shots arrive late. It disguises itself as a graphics problem perfectly: only while moving, identical at every quality setting, desktop fine. THREE.Audio/PositionalAudio on one AudioListener have none of this \u2014 genex-ai-sfx ships the pooled one-shot, the live-value loop, and the decode-during-the-menu preload.`
|
|
5103
|
+
);
|
|
5104
|
+
}
|
|
5082
5105
|
for (const d of s.depthRange) {
|
|
5083
5106
|
log.warn(
|
|
5084
5107
|
`Camera depth range ${d.detail} (far/near = ${d.ratio.toExponential(1)}) at ${d.where} is past what a 24-bit depth buffer can serve \u2014 touching surfaces will z-fight and flicker, and it reads fine right up until it doesn't. near is the expensive knob (resolvable depth \u2248 z\xB2 / (near \xD7 2^24)), so raise near to the closest the camera can actually get. If the range is genuinely astronomical, that is what WebGLRenderer's logarithmicDepthBuffer is for: genex-threejs-camera-direction.`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.99.0-dev.286",
|
|
4
4
|
"description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -87,6 +87,11 @@ document.addEventListener("visibilitychange", () => {
|
|
|
87
87
|
});
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
Decode the track (and every sfx clip) while the **menu** is up, not on PLAY:
|
|
91
|
+
`loadAsync` runs `decodeAudioData`, which does not need a running `AudioContext` —
|
|
92
|
+
only playing does. `$genex-ai-sfx` has the preload-everything recipe; run the
|
|
93
|
+
music through the same pass so the first second of gameplay waits on nothing.
|
|
94
|
+
|
|
90
95
|
**Menu reuse:** the same `music` object at lower volume (`setVolume(0.18)`)
|
|
91
96
|
during the menu phase, restored to the setting's value on PLAY — zero extra
|
|
92
97
|
generations. If the loop point is audible (the track came back with an
|
|
@@ -98,9 +103,11 @@ intro/outro despite the prompt), crossfade the tail into the head with a second
|
|
|
98
103
|
A game with generated audio and no volume control is a failure mode. The
|
|
99
104
|
settings screen gets **two sliders — Music and SFX** (music default 0.30, sfx
|
|
100
105
|
~0.7), persisted (localStorage or the save slot) and applied live:
|
|
101
|
-
`music.setVolume(v)` for the music bed
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
`music.setVolume(v)` for the music bed, and one shared `sfxVolume` multiplier
|
|
107
|
+
every sound effect is scaled by (the `playSfx` / live-value recipes in
|
|
108
|
+
`$genex-ai-sfx` already read it — keep it a single exported value, never a
|
|
109
|
+
number copied into each call site). `$genex-threejs-game-ui` carries this as a
|
|
110
|
+
hard settings rule — wire the sliders in the same milestone as the track.
|
|
104
111
|
|
|
105
112
|
## Publish checklist
|
|
106
113
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: genex-ai-sfx
|
|
3
|
-
description: Generate a real sound effect (mp3) from a text prompt with `npx genex sfx`, then
|
|
3
|
+
description: Generate a real sound effect (mp3) from a text prompt with `npx genex sfx`, then wire it in Three.js — one-shots, rapid repeats, and a looping sound whose pitch and volume track a live value (engine, wheels, wind). Use for any specific sound triggered by a game event, and whenever audio must react continuously to gameplay without stuttering phones.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Genex AI · SFX
|
|
@@ -54,8 +54,105 @@ if (!sound.isPlaying) sound.play();
|
|
|
54
54
|
**Global** (UI/non-spatial — same volume everywhere): use `new THREE.Audio(listener)`
|
|
55
55
|
instead of `PositionalAudio` and don't attach it to a mesh.
|
|
56
56
|
|
|
57
|
-
Reuse one loaded `buffer` across many plays
|
|
58
|
-
|
|
57
|
+
Reuse one loaded `buffer` across many plays — the three recipes below are how.
|
|
58
|
+
|
|
59
|
+
## Never drive game sound through an `<audio>` element
|
|
60
|
+
|
|
61
|
+
`new Audio(url)`, `document.createElement("audio")`, an `<audio>` tag — same object,
|
|
62
|
+
all wrong here. On iPhone it is a **handle on the OS media pipeline in another
|
|
63
|
+
process**: writing `.volume`/`.playbackRate` is a **cross-process call** (a sound
|
|
64
|
+
driven from a live value writes 60–120 of those a second and stutters the whole
|
|
65
|
+
game), and `play()` is **async and slow**, so one-shots land after the event.
|
|
66
|
+
|
|
67
|
+
It disguises itself as a graphics problem: only while moving, identical at every
|
|
68
|
+
quality setting, desktop fine. The one-second tell — **the music plays throughout
|
|
69
|
+
and costs nothing**, same object and same code path, because its volume and pitch
|
|
70
|
+
never change. *A sound playing is free; a sound written to every frame is not.*
|
|
71
|
+
|
|
72
|
+
`THREE.Audio`/`PositionalAudio` are WebAudio and have none of this — use them for
|
|
73
|
+
everything, including UI clicks.
|
|
74
|
+
|
|
75
|
+
## Decode everything before the player needs it
|
|
76
|
+
|
|
77
|
+
A clip that hasn't finished decoding stalls on its first play — exactly the first
|
|
78
|
+
second of gameplay. **Decoding needs no running `AudioContext`, only playing does**,
|
|
79
|
+
so decode while the menu is up:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import * as THREE from "three";
|
|
83
|
+
|
|
84
|
+
const listener = new THREE.AudioListener();
|
|
85
|
+
camera.add(listener); // ONE listener serves every sound and the music
|
|
86
|
+
|
|
87
|
+
const CLIPS = {
|
|
88
|
+
jump: "https://assets.genex.technology/generations/<id>/audio-sfx",
|
|
89
|
+
wheels: "https://assets.genex.technology/generations/<id>/audio-sfx",
|
|
90
|
+
} as const;
|
|
91
|
+
type ClipName = keyof typeof CLIPS;
|
|
92
|
+
|
|
93
|
+
const buffers = new Map<ClipName, AudioBuffer>();
|
|
94
|
+
|
|
95
|
+
/** Call this while the menu/loading screen is up — no user gesture needed. */
|
|
96
|
+
export async function preloadAudio(): Promise<void> {
|
|
97
|
+
const loader = new THREE.AudioLoader();
|
|
98
|
+
await Promise.all(
|
|
99
|
+
Object.entries(CLIPS).map(async ([name, url]) => {
|
|
100
|
+
buffers.set(name as ClipName, await loader.loadAsync(url));
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Rapid repeats: pool the voices
|
|
107
|
+
|
|
108
|
+
Footsteps, hits and gunfire re-trigger faster than a clip ends, one `THREE.Audio`
|
|
109
|
+
plays once at a time, and a fresh one per shot allocates during combat. Keep a
|
|
110
|
+
small pool per clip and reuse whatever is idle:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const pool = new Map<ClipName, THREE.Audio[]>();
|
|
114
|
+
|
|
115
|
+
export function playSfx(name: ClipName, volume = 0.7): void {
|
|
116
|
+
const buffer = buffers.get(name);
|
|
117
|
+
if (!buffer) return; // still decoding — silence beats a stall
|
|
118
|
+
const voices = pool.get(name) ?? [];
|
|
119
|
+
let voice = voices.find((v) => !v.isPlaying);
|
|
120
|
+
if (!voice) {
|
|
121
|
+
voice = new THREE.Audio(listener);
|
|
122
|
+
voice.setBuffer(buffer);
|
|
123
|
+
voices.push(voice);
|
|
124
|
+
pool.set(name, voices);
|
|
125
|
+
}
|
|
126
|
+
voice.setVolume(volume * sfxVolume); // sfxVolume = the settings slider
|
|
127
|
+
voice.play();
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Positional one-shots: same pool built from `THREE.PositionalAudio`, `mesh.add(voice)`.
|
|
132
|
+
|
|
133
|
+
## A sound that tracks a live value
|
|
134
|
+
|
|
135
|
+
Engine, wheels, wind, rotor wash, a charging weapon — start the loop **once**, then
|
|
136
|
+
write to it every frame. Both calls below are AudioParam writes on the audio thread:
|
|
137
|
+
no allocation, no cross-process call.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const wheels = new THREE.Audio(listener);
|
|
141
|
+
wheels.setBuffer(buffers.get("wheels")!);
|
|
142
|
+
wheels.setLoop(true);
|
|
143
|
+
wheels.setVolume(0);
|
|
144
|
+
wheels.play(); // ONCE, from the first user gesture — never per frame
|
|
145
|
+
|
|
146
|
+
// in the render loop:
|
|
147
|
+
const speed01 = THREE.MathUtils.clamp(speed / topSpeed, 0, 1);
|
|
148
|
+
wheels.setVolume(speed01 * 0.6 * sfxVolume);
|
|
149
|
+
wheels.setPlaybackRate(0.8 + speed01 * 0.7); // pitch rises with speed
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
If the driving value can jump (a gear change, a hit, a respawn), a step is an
|
|
153
|
+
audible click — glide instead:
|
|
154
|
+
`wheels.gain.gain.setTargetAtTime(target, listener.context.currentTime, 0.05)`.
|
|
155
|
+
To silence a loop, ride the volume to 0 and leave it running — never `stop()`/`play()`.
|
|
59
156
|
|
|
60
157
|
## Publish checklist
|
|
61
158
|
|
|
@@ -86,3 +183,8 @@ Reuse one loaded `buffer` across many plays; create a fresh `Audio`/`PositionalA
|
|
|
86
183
|
first play from a click/keydown. Confirm the camera has an `AudioListener`.
|
|
87
184
|
- **Too quiet/loud** — `sound.setVolume(0..1)`; for positional, tune
|
|
88
185
|
`setRefDistance` / `setRolloffFactor`.
|
|
186
|
+
- **Stutters on a phone while moving, at every quality setting** — audio, not
|
|
187
|
+
graphics: see the `<audio>` section above (`$genex-threejs-adaptive-quality` has
|
|
188
|
+
the same triage from the performance side).
|
|
189
|
+
- **A sound is late the first time, then fine** — it was still decoding. Preload
|
|
190
|
+
during the menu.
|
|
@@ -17,6 +17,28 @@ This is a completion gate like the post stack: every game wires the tier at
|
|
|
17
17
|
boot before it is called done. It costs three lines, not a testing burden —
|
|
18
18
|
you still verify on desktop only.
|
|
19
19
|
|
|
20
|
+
## Arriving here because a phone stutters? Rule out audio first
|
|
21
|
+
|
|
22
|
+
Not every mobile performance problem is a rendering problem, and one impostor
|
|
23
|
+
costs whole afternoons — shadows, bloom, draw calls, physics and memory all get
|
|
24
|
+
investigated before anyone looks at the sound. Read these three symptoms
|
|
25
|
+
together before you touch a single quality knob:
|
|
26
|
+
|
|
27
|
+
- it only happens **while moving** (or while some value is continuously changing),
|
|
28
|
+
- it is **identical at every quality setting**, including the lowest,
|
|
29
|
+
- **desktop is fine.**
|
|
30
|
+
|
|
31
|
+
That is the signature of an `<audio>` element being written to every frame — on
|
|
32
|
+
iPhone each `.volume`/`.playbackRate` write is a call into another process. The
|
|
33
|
+
one-second confirmation: **the music plays throughout and costs nothing**, because
|
|
34
|
+
music's volume and pitch never change, so nothing is ever written to it. Same
|
|
35
|
+
object, same code path, opposite cost. `$genex-ai-sfx` has the fix and the
|
|
36
|
+
WebAudio recipes; nothing in this skill will help.
|
|
37
|
+
|
|
38
|
+
Genuine graphics symptoms look different: a quality setting changes it, it is
|
|
39
|
+
worse when more is on screen, and it degrades over minutes (thermal) rather than
|
|
40
|
+
tracking one input.
|
|
41
|
+
|
|
20
42
|
## Install
|
|
21
43
|
|
|
22
44
|
```bash
|
|
@@ -205,3 +227,6 @@ runtime).
|
|
|
205
227
|
- Testing quality tiers by resizing the desktop window → tiers key off touch +
|
|
206
228
|
OS, not viewport. Trust desktop verification plus the preflight report
|
|
207
229
|
`genex preview` prints.
|
|
230
|
+
- Stepping quality down to chase a stutter the quality settings never change →
|
|
231
|
+
that is the audio impostor above, and every rung you spend on it makes the
|
|
232
|
+
game uglier without touching the cause.
|
|
@@ -167,3 +167,9 @@ propeller: `finalThrottle` (0..1 mixer output — rotor-wash/audio intensity),
|
|
|
167
167
|
`worldThrustPos`/`worldThrustDir` (where and which way to emit), and
|
|
168
168
|
`thrustImpulse`. `drone.hoverThrottle` reads the last computed hover value.
|
|
169
169
|
All vectors are reused internal instances — copy, never mutate.
|
|
170
|
+
|
|
171
|
+
Driving rotor audio from `finalThrottle` is a per-frame write, so it must go
|
|
172
|
+
through WebAudio: start ONE looped `THREE.Audio` and call
|
|
173
|
+
`setVolume`/`setPlaybackRate` on it each frame — never an `<audio>` element,
|
|
174
|
+
whose `.volume`/`.playbackRate` writes are cross-process calls on iPhone and
|
|
175
|
+
stutter the whole game. `$genex-ai-sfx` has the live-value loop recipe.
|