@genex-ai/cli-demo 0.98.0 → 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 CHANGED
@@ -8,7 +8,7 @@ import fs from "fs";
8
8
  import os from "os";
9
9
  import path from "path";
10
10
  import { fileURLToPath } from "url";
11
- var RAW_CHANNEL = "latest";
11
+ var RAW_CHANNEL = "dev";
12
12
  var CLI_CHANNEL = RAW_CHANNEL === "dev" ? "dev" : "latest";
13
13
  var STANDS = {
14
14
  prod: { api: "https://api.genex.games", dashboard: "https://genex.games" },
@@ -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 = { guessedRepeat: [], squarePoints: [], depthRange: [] };
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.98.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; keep every sfx's volume routed through
102
- one shared multiplier. `$genex-threejs-game-ui` carries this as a hard settings
103
- rule wire the sliders in the same milestone as the track.
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 play it in Three.js (positional or global audio). Use when the user wants a specific sound a gunshot, footstep, pickup chime, explosion, engine hum, UI click, whoosh triggered on a game event.
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; create a fresh `Audio`/`PositionalAudio`
58
- (or call `sound.play()` again once stopped) per trigger.
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.
@@ -116,7 +116,7 @@ scene.background = texture; // keep the raw texture for the visible sky
116
116
 
117
117
  ## Troubleshooting
118
118
 
119
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
119
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
120
120
  - **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
121
121
  this skybox generation. Tell the user the facts the CLI printed: their balance, this
122
122
  generation's cost, and when their credits refill. Then offer to continue the build
@@ -225,7 +225,7 @@ first one is the one a screenshot of the whole arena will not show you.
225
225
 
226
226
  ## Troubleshooting
227
227
 
228
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
228
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
229
229
  - **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
230
230
  this texture generation. Tell the user the facts the CLI printed: their balance,
231
231
  this generation's cost, and when their credits refill. Then offer to continue the
@@ -151,7 +151,7 @@ set belongs to `$genex-ai-hud` — both build on `npx genex image`/`video`.
151
151
 
152
152
  ## Troubleshooting
153
153
 
154
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
154
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
155
155
  - **"Prompt rejected"** — the provider's content-safety filter blocked the prompt.
156
156
  This is non-retryable; retrying the same wording fails again. Rewrite the prompt.
157
157
  - **Nothing plays / black surface** — the first `video.play()` must run inside a user
@@ -132,7 +132,7 @@ and re-link the clone to the same live game:
132
132
  ```bash
133
133
  git clone <the game's repo url> my-game && cd my-game
134
134
  npm install
135
- npx @genex-ai/cli-demo@latest link <slug> # slug = the name in the play URL
135
+ npx @genex-ai/cli-demo@dev link <slug> # slug = the name in the play URL
136
136
  ```
137
137
 
138
138
  Don't know the slug? **`npx genex list`** prints every game on your account —
@@ -159,7 +159,7 @@ Safe to run any time — genex-owned skills are refreshed to the latest version,
159
159
  and your own files are never touched:
160
160
 
161
161
  ```bash
162
- npx @genex-ai/cli-demo@latest init
162
+ npx @genex-ai/cli-demo@dev init
163
163
  ```
164
164
 
165
165
  Use `--force` only if you intentionally want your own existing files overwritten
@@ -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.
@@ -38,7 +38,7 @@ update, so update immediately.)
38
38
  Run exactly the command the nudge printed, from the game project root:
39
39
 
40
40
  ```bash
41
- npm i -D @genex-ai/cli-demo@latest # the genex CLI (a dev dependency)
41
+ npm i -D @genex-ai/cli-demo@dev # the genex CLI (a dev dependency)
42
42
  npm i @genex-ai/embed-sdk@latest # identity/saves SDK (ships inside the game)
43
43
  npm i @genex-ai/multiplayer@latest # multiplayer SDK (only if the game uses it)
44
44
  ```