@hypersoniclabs/helix-mcp 0.2.5 → 0.2.12

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.
Files changed (98) hide show
  1. package/README.md +81 -11
  2. package/dist/continuumCanary.d.ts +17 -0
  3. package/dist/continuumCanary.js +17 -0
  4. package/dist/continuumCanary.js.map +1 -0
  5. package/dist/server.d.ts +14 -1
  6. package/dist/server.js +4058 -162
  7. package/dist/server.js.map +1 -1
  8. package/dist/tsconfig.build.tsbuildinfo +1 -1
  9. package/dist/vehicleTools.d.ts +86 -0
  10. package/dist/vehicleTools.js +229 -0
  11. package/dist/vehicleTools.js.map +1 -0
  12. package/docs/avatar-face.md +115 -0
  13. package/docs/bridge.md +98 -0
  14. package/docs/bring-your-world.md +117 -0
  15. package/docs/catalog.md +69 -1
  16. package/docs/character-animation.md +442 -0
  17. package/docs/character-attachments.md +166 -0
  18. package/docs/character-world.md +785 -130
  19. package/docs/continuum.md +153 -0
  20. package/docs/items.md +73 -0
  21. package/docs/lighting-world.md +667 -0
  22. package/docs/locomotion-clip-spec.md +294 -0
  23. package/docs/manifest.md +31 -6
  24. package/docs/multiplayer-logic.md +460 -17
  25. package/docs/multiplayer-templates/chrono-orchard.md +36 -22
  26. package/docs/multiplayer-templates/collect-a-thon.md +28 -38
  27. package/docs/multiplayer-templates/collections.md +24 -24
  28. package/docs/multiplayer-templates/hangout.md +124 -111
  29. package/docs/multiplayer-templates/npc-wave.md +310 -0
  30. package/docs/multiplayer-templates/obby.md +13 -16
  31. package/docs/multiplayer-templates/persistent-progress.md +218 -0
  32. package/docs/multiplayer-templates/physics-bumper.md +27 -9
  33. package/docs/multiplayer-templates/physics-football.md +22 -8
  34. package/docs/multiplayer-templates/relic-bearers.md +12 -15
  35. package/docs/multiplayer-templates/server-motion.md +16 -19
  36. package/docs/multiplayer-templates/shooter-range.md +275 -0
  37. package/docs/multiplayer-templates/team-control.md +28 -15
  38. package/docs/multiplayer-templates/turn-arena.md +31 -22
  39. package/docs/multiplayer-templates/voice-radio.md +166 -0
  40. package/docs/multiplayer-templates/wave-survival.md +7 -8
  41. package/docs/multiplayer-templates/world-shop.md +240 -0
  42. package/docs/multiplayer-world.md +222 -129
  43. package/docs/npc-world.md +623 -0
  44. package/docs/publishing.md +108 -28
  45. package/docs/purchases.md +223 -0
  46. package/docs/scene-performance.md +64 -0
  47. package/docs/screenshots.md +140 -0
  48. package/docs/sdk.md +324 -5
  49. package/docs/shooter-worlds.md +537 -0
  50. package/docs/terrain.md +173 -0
  51. package/docs/upgrades.md +324 -0
  52. package/docs/vehicles.md +727 -0
  53. package/docs/world-inspect.md +156 -0
  54. package/docs/world-look.md +241 -0
  55. package/docs/world-recipe.md +65 -6
  56. package/package.json +15 -4
  57. package/skills/README.md +91 -0
  58. package/skills/helix-assets/SKILL.md +491 -0
  59. package/skills/helix-assets/references/asset-sources.md +143 -0
  60. package/skills/helix-assets/references/vault-api.md +105 -0
  61. package/skills/helix-avatar-qa/SKILL.md +85 -0
  62. package/skills/helix-avatars/SKILL.md +206 -0
  63. package/skills/helix-avatars/references/contract.md +166 -0
  64. package/skills/helix-avatars/references/dynamics.md +367 -0
  65. package/skills/helix-avatars/references/face.md +50 -0
  66. package/skills/helix-avatars/references/publish.md +76 -0
  67. package/skills/helix-avatars/references/qa.md +251 -0
  68. package/skills/helix-avatars/references/rigging.md +88 -0
  69. package/skills/helix-avatars/references/source-generated.md +190 -0
  70. package/skills/helix-avatars/references/source-model.md +90 -0
  71. package/skills/helix-avatars/references/source-rigid.md +90 -0
  72. package/skills/helix-avatars/references/source-vrm.md +61 -0
  73. package/skills/helix-gauntlet/SKILL.md +128 -0
  74. package/skills/helix-multiplayer/SKILL.md +150 -0
  75. package/skills/helix-multiplayer/references/dsl-capability-map.md +107 -0
  76. package/skills/helix-vehicles/SKILL.md +218 -0
  77. package/skills/helix-vehicles/references/addons.md +212 -0
  78. package/skills/helix-vehicles/references/appearance.md +339 -0
  79. package/skills/helix-vehicles/references/audio-import.md +138 -0
  80. package/skills/helix-vehicles/references/audio.md +580 -0
  81. package/skills/helix-vehicles/references/cabin.md +225 -0
  82. package/skills/helix-vehicles/references/host-manifest.md +174 -0
  83. package/skills/helix-vehicles/references/physics-from-specs.md +463 -0
  84. package/skills/helix-vehicles/references/publish.md +214 -0
  85. package/skills/helix-vehicles/references/qa.md +177 -0
  86. package/skills/helix-vehicles/references/reference-package.json +3481 -0
  87. package/skills/helix-vehicles/references/reference-package.md +69 -0
  88. package/skills/helix-vehicles/references/source-beamng.md +167 -0
  89. package/skills/helix-vehicles/references/source-concept.md +38 -0
  90. package/skills/helix-vehicles/references/source-model.md +100 -0
  91. package/skills/helix-vehicles/references/source-scratch.md +60 -0
  92. package/skills/helix-world-build/SKILL.md +376 -0
  93. package/skills/helix-world-build/references/config-gates.md +104 -0
  94. package/skills/helix-world-director/SKILL.md +210 -0
  95. package/skills/helix-world-qa/SKILL.md +371 -0
  96. package/skills/helix-world-qa/references/perf-budgets.md +240 -0
  97. package/skills/helix-world-qa/references/perf-handle.md +125 -0
  98. package/skills/helix-world-qa/references/visual-scorecard.md +85 -0
@@ -0,0 +1,580 @@
1
+ # Vehicle audio
2
+
3
+ The contract is in section 10 of `read_doc({ name: "vehicles" })`. This reference covers how to
4
+ fill it.
5
+
6
+ ## The hard rule (it overrides every older note)
7
+
8
+ **Never generate or synthesise a vehicle sound.** Not spectral resynthesis, not an oscillator or
9
+ physical model, not `generate_audio` (not even for a one-shot), not the engine's built-in
10
+ synthesised or generated defaults, and not for a slot no source provides. **Always source real
11
+ recorded sound, from BeamNG mods as much as possible, and a car ships a sourced recording for
12
+ EVERY runtime slot** (the full table is in "Every slot, every car", below). `check_vehicle_audio`
13
+ and `publish_vehicle` refuse audio that is synthesised or generated (`AUDIO_SYNTHESISED`), fail a
14
+ car with no sourced engine audio (`AUDIO_ENGINE_UNSOURCED`), and fail **any empty slot and any
15
+ slot that would fall back to a platform default** (`AUDIO_SLOT_UNSOURCED`). The only permitted empty slot is a
16
+ component the real car physically lacks (see "The only empty slot"); nothing is ever filled with a
17
+ synthesised stand-in or with another car's sound under this car's label.
18
+
19
+ ## Step one, whatever the mesh source
20
+
21
+ The mesh may come from anywhere (a BeamNG mod, a downloaded model, a scan). The audio does not
22
+ follow it. **Find the most accurate simulator mod of that exact car and extract its audio**:
23
+
24
+ 1. **BeamNG first** (the mod of that exact car; its `art/sound` blends, FMOD events and the
25
+ `soundscape`). Then, where theirs is more accurate for this car: **Assetto Corsa / ACC**
26
+ (`content/cars/<car>/sfx/<car>.bank` and its `GUIDs.txt`), **rFactor 2**, **Automobilista 2**.
27
+ 2. Extract with the bundled CLI:
28
+
29
+ ```
30
+ import_vehicle_audio({ source: "/abs/mod-or-car-dir-or.zip-or.bank", out: "/abs/car", sim: "beamng",
31
+ vehicle: "<beamng vehicle id>", config: "<file.pc>", beamngRoot: "<BeamNG.drive dir>",
32
+ cylinders: 8, modUrl: "https://…", basePackage: "/abs/car/vehicle-package.json" })
33
+ # the same call from a shell:
34
+ helix vehicle audio-import <mod dir | mod .zip | AC car dir | .bank> --out <dir> [--sim beamng|ac|auto] [--vehicle <id>]
35
+ [--config <file.pc>] [--map <slot-map.json>] [--beamng-root <BeamNG.drive>] [--cylinders N]
36
+ [--prefer exterior|interior] [--ac-native-rungs] [--mod-url <url>] [--base-package <pkg>] [--json]
37
+ ```
38
+
39
+ It writes `<out>/audio/*.wav` (16-bit PCM), `<out>/audio.json` (the package `audio` block with
40
+ **all 31 slots**: `bankId`, `sampleCylinderCount`, `strokes`, `engineBlend`, `exhaustBlend?`,
41
+ `bank`, `slots`; copy `audio/` beside `vehicle-package.json` and drop the block in) and
42
+ `<out>/audio-import.json` (the report: every slot's decision, source, alternatives and reason,
43
+ the unmapped source sounds, the stock events extracted). **Every consumed slot, slot asset, bank
44
+ sample and blend carries a `provenance`** (see "Provenance", below), inline, and it survives
45
+ publish.
46
+ - Options in short: `config` (the car configuration, whose `soundConfig` is wanted), `vehicle`,
47
+ `beamngRoot` (the BeamNG.drive install, so stock events and base-game defaults resolve),
48
+ `cylinders` (the RECORDED engine's count; **required for an Assetto Corsa car and for a
49
+ BeamNG mod whose count would only be a guess**), `prefer`, `acNativeRungs`, `modUrl`,
50
+ `basePackage` (re-importing a published car: keep its current mix). Every option, the decode
51
+ tools and the mix fields are in `read_skill({ name: "helix-vehicles", reference: "audio-import" })`.
52
+ - `map` builds one car from several sources ("`--map`", below).
53
+ 3. Map every extracted sound to the HELIX slots (next section) and run
54
+ `check_vehicle_audio({ target: <dir> })`.
55
+
56
+ If the mesh came from a BeamNG mod, `bridge_import` already extracts that mod's own engine
57
+ ladders. Treat that as the first candidate, not the answer: you still compare it with other
58
+ candidates (next section), and you still run `import_vehicle_audio` for every slot the bridge did
59
+ not fill and to stamp provenance.
60
+
61
+ ## Which mod is "most accurate"? Measure it, do not trust the mod's reputation
62
+
63
+ Collect **at least two candidates** where they exist (several BeamNG mods of the car, an Assetto
64
+ Corsa version, a stock BeamNG engine blend of the same engine), and **two or three real recordings
65
+ of the car** (steady idle, a steady high-rpm hold or a pull, the rev limiter, a cold start; onboard
66
+ or close exterior; fetch with `yt-dlp -x --audio-format wav <url>` and cut 10-30 s with
67
+ `ffmpeg -ss 12 -t 20 -i in.wav -ac 1 -ar 44100 -c:a pcm_s16le clip.wav`). Then measure the same
68
+ five things on a candidate's rungs and on the real clips, with the same script, and read them side
69
+ by side:
70
+
71
+ | Measure | What to compare | Candidate is accurate when |
72
+ | --- | --- | --- |
73
+ | **Firing-fundamental pitch track vs rpm** | each rung's f0 against its rpm label, and the real clips' f0 against the rpm they were at (`f0 = rpm x C / 120` even-firing; flat-plane V8 `rpm/15`; cross-plane V8 `rpm/60`) | every rung within 10 % of its label's pitch, f0 rising monotonically with rpm, and the idle and redline f0 matching the real car's idle and limiter rpm |
74
+ | **Spectral centroid** | idle rung and top rung against the real idle clip and the real high-rpm clip | within 30 % of the real car at idle and at the top, and rising with rpm |
75
+ | **Roughness / harmonic balance** | 8-60 Hz firing-modulation depth, and which orders carry the energy (a spectrogram of both) | roughness within x2 of the real car, the same dominant orders |
76
+ | **Idle character** | idle rpm, lope, and cylinder-to-cylinder unevenness (the dominant envelope-modulation frequency at idle) | same idle rpm and the same lope as the real idle |
77
+ | **Limiter cadence** | cuts per second when the car sits on its limiter (the dominant envelope-modulation frequency of the limiter clip) | within 25 % of the real car's cadence at the same limiter rpm |
78
+
79
+ Field recordings of one car differ by x2-4 between takes (microphone distance, wind, EQ), so use
80
+ the **median of several clips** and read a near miss as "listen and look at both spectrograms",
81
+ not as proof. A mod that wins on pitch and loses on timbre, or the reverse, is not accurate: pick
82
+ the candidate with the most measures inside tolerance, and write the measurements for every
83
+ candidate into your ledger. Then **listen** to the winner beside a real clip.
84
+
85
+ The measuring script (numpy + scipy; `ffmpeg` on the PATH for mp3/m4a input). It searches for the
86
+ firing fundamental only inside the rpm range you give it, so it cannot latch onto a sub-harmonic:
87
+
88
+ ```python
89
+ #!/usr/bin/env python3
90
+ # measure-engine.py <clip> --cyl 8 [--strokes 4] [--layout flat|cross] [--rpm 600 9000]
91
+ import argparse, subprocess, numpy as np
92
+ from scipy.io import wavfile
93
+ ap = argparse.ArgumentParser(); ap.add_argument('clip'); ap.add_argument('--cyl', type=int, required=True)
94
+ ap.add_argument('--strokes', type=int, default=4); ap.add_argument('--layout', default='flat')
95
+ ap.add_argument('--rpm', type=float, nargs=2, default=[500, 9000]); a = ap.parse_args()
96
+ wav = a.clip
97
+ if not wav.lower().endswith('.wav'):
98
+ wav = '/tmp/_measure.wav'; subprocess.run(['ffmpeg', '-y', '-v', 'error', '-i', a.clip, '-ac', '1', '-ar', '44100', wav], check=True)
99
+ sr, x = wavfile.read(wav); x = x.astype(np.float64)
100
+ if x.ndim > 1: x = x.mean(1)
101
+ x /= (np.abs(x).max() or 1)
102
+ orders = 1.0 if a.layout == 'cross' else (a.cyl / 2 if a.strokes == 4 else a.cyl) # f0 = rpm/60 x orders
103
+ N = int(0.372 * sr); fr = np.fft.rfftfreq(N, 1 / sr)
104
+ cand = np.arange(a.rpm[0] / 60 * orders * 0.9, a.rpm[1] / 60 * orders * 1.1, 0.5)
105
+ print('t_s f0_Hz rpm centroid_Hz')
106
+ for s in range(0, len(x) - N, int(0.25 * sr)):
107
+ sp = np.log1p(np.abs(np.fft.rfft(x[s:s + N] * np.hanning(N), n=2 * N)) * 50); df = sr / (2 * N)
108
+ score = [sum(sp[max(int(round(k * f / df)) - 1, 0):int(round(k * f / df)) + 2].max() / k ** 0.3
109
+ for k in range(1, 7) if k * f / df < len(sp) - 2) for f in cand]
110
+ f0 = cand[int(np.argmax(score))]; mag = np.abs(np.fft.rfft(x[s:s + N] * np.hanning(N)))
111
+ print(f'{s / sr:5.1f} {f0:8.1f} {f0 * 60 / orders:6.0f} {(mag * fr).sum() / (mag.sum() + 1e-9):8.0f}')
112
+ env = np.abs(x); e = np.abs(np.fft.rfft(env - env.mean())); ef = np.fft.rfftfreq(len(env), 1 / sr)
113
+ b = (ef >= 8) & (ef <= 60)
114
+ print('roughness (8-60 Hz envelope modulation / mean level): %.4f' % (np.sqrt((e[b] ** 2).sum()) / (len(env) * env.mean() + 1e-9)))
115
+ c = (ef >= 2) & (ef <= 40)
116
+ print('dominant envelope modulation: %.1f Hz (idle lope, or limiter cut cadence on a limiter clip)' % ef[c][np.argmax(e[c])])
117
+ ```
118
+
119
+ Control: a real F40 idle clip reads f0 about 64 Hz = 960 rpm (V8 flat-plane, `--cyl 8 --rpm 600 8000`),
120
+ centroid about 1.1 kHz, roughness about 0.15. Run it the same way on a candidate's idle rung.
121
+
122
+ **Never choose by reputation, download count or file size, and never keep a mod you did not
123
+ measure.** A mod can ship a lovely 1,000 rpm loop and a top rung that is two octaves off.
124
+
125
+ ## Every slot, every car
126
+
127
+ The engine defines **31 audio slots** (`VehicleAudioSlot` in the vehicle package format). A car
128
+ ships **a sourced recording for every one**, each with its `provenance`. `consumed` says whether
129
+ the runtime plays it today; it never says whether the sound is needed. A slot the runtime does not
130
+ play (today `idle`, which the runtime idles on the ladder for, `transmissionWhine`, which it plays
131
+ only with a declared native pitch, and `intake`, for which it has no layer) still carries its
132
+ sourced recording, unconsumed, with the reason.
133
+
134
+ Take each from, in order: **the car's own BeamNG mod; the BeamNG base game** (stock FMOD events
135
+ in `content/art_sound.zip` `art/sound/fmod/desktop/vehicle.bank` and `vehicle_preload.bank` (with
136
+ their `.assets` / `.streams` banks), `content/vehicles/common.zip` `vehicles/common/sounds/` (for
137
+ example `turbo_01.ogg`, `turbo_03.ogg`, `turbo_whistle.ogg`, `turbo_bov.ogg`, `V8_default.ogg`) and
138
+ its `soundscape.jbeam`, and the stock blends in `content/audio.zip`); **Assetto Corsa / ACC** car
139
+ banks (`content/cars/<car>/sfx/<car>.bank`). List a bank's events first (`vgmstream-cli -m`, or the import's own
140
+ listing where it prints one) and pick by role. The column below says where each slot
141
+ usually comes from.
142
+
143
+ | Slot | What it is | Where it usually comes from |
144
+ | --- | --- | --- |
145
+ | `engineLadder` | Engine-bay rpm ladder; row 0 off-throttle/low load, row 1 on-throttle | The car's BeamNG mod blend (`art/sound/blends/*.sfxBlend2D.json` + `art/sound/engine/<name>/NNN_RRRRR.flac`); AC `engine` event. **Separate from the exhaust blend.** |
146
+ | `exhaustLadder` | Tailpipe rpm ladder, load rows as above | The mod's exhaust blend (`soundConfigExhaust.sampleName`). A mod with ONE combined recording (the mmbng 993 sets the exhaust `sampleName` to `"0"`) omits `exhaustBlend` and declares this slot `absent: "combined-with-engine"`; AC has one combined `engine_ext` set |
147
+ | `idle` | Steady idle loop | The mod's idle loop, else the ladder's bottom rungs from the same source |
148
+ | `startup` | Starter crank and catch | The mod's starter event; the base game's starter events in `vehicle.bank`; AC engine-start event |
149
+ | `shutdown` | Key-off | The mod's shutdown event; the base game's engine-off event |
150
+ | `shiftUp`, `shiftDown` | Gear change thump/clunk | The mod's shift events; the base game's shift events; AC gearbox events |
151
+ | `clutch` | Pedal click / engagement | The mod's clutch event; the base game's clutch event; AC clutch event |
152
+ | `transmissionWhine` | Gearbox/diff whine loop on road speed | The mod's gearbox whine; the base game's gearbox/diff loops; AC transmission event |
153
+ | `turboSpool` | Turbo spool loop on shaft rpm | The mod's turbo events; base `vehicles/common/sounds/turbo_01.ogg`, `turbo_03.ogg`, `turbo_whistle.ogg`; AC turbo event |
154
+ | `blowOff` | Blow-off / wastegate dump | The mod's blow-off; base `vehicles/common/sounds/turbo_bov.ogg`; AC blow-off event |
155
+ | `intake` | Induction noise | The mod's intake event; the base game's intake event; AC intake event |
156
+ | `revLimiter` | One short fuel-cut crack, retriggered at the limiter | The mod's limiter event; the base game's limiter / misfire events |
157
+ | `backfire` | Overrun pop / bang | The mod's backfire; the base game's backfire events; AC backfire event |
158
+ | `suspension` | Bump / suspension knock | The base game's suspension events; the mod's own |
159
+ | `impactLight`, `impactMedium`, `impactHeavy` | Collision thumps by severity | The base game's collision events (`vehicle.bank`); the mod's own |
160
+ | `tyreSqueal` | Tyre squeal on slip (the aggregate / brakes-and-skid squeal) | The base game's tyre/skid events; AC skid event |
161
+ | `tyreScrub` | Slip loop (lateral scrub) | The base game's tyre-slip events; AC skid event |
162
+ | `tyreSpin` | Wheelspin loop | The base game's wheelspin / tyre-spin events |
163
+ | `tyreRollSlow`, `tyreRollFast` | Rolling noise, crossfaded by speed | The base game's tyre-roll / road-noise events; AC tyre events |
164
+ | `indicatorTick` | Indicator relay click | The mod's indicator event; the base game's indicator event |
165
+ | `skid` | Skid onset | The base game's skid events; AC skid event |
166
+ | `brakeSqueal` | Light-braking squeal | The base game's brake events; AC brake event |
167
+ | `handbrake` | Handbrake ratchet / pull | The base game's handbrake event; the mod's own |
168
+ | `reverseBeep` | Reverse warning | The mod's reverse beep (trucks, vans); the base game's reverse-warning event. A car with none is the only case under "The only empty slot" |
169
+ | `horn` | The car's horn; plays when the specialty declares a horn action | The car's own horn event; the base game's horn events only when the mod has none of its own |
170
+ | `doorOpen`, `doorClose` | Door open / close | The mod's door events; the base game's door events |
171
+
172
+ `import_vehicle_audio` resolves the stock banks and blends when you pass `beamngRoot`. **Row 0 / row 1** of each ladder are the load axis.
173
+ **Doors are required** (`doorOpen`, `doorClose`): a sourced recording each, from the mod's door
174
+ couplers or the base game's door latches; the engine is wiring the door edge. **There is no bonnet,
175
+ boot or engine-cover slot yet** (the importer reports a mod's bonnet and boot latches as unmapped):
176
+ do not invent a slot and do not promise a panel sound.
177
+
178
+ **A different car's sound under this car's label is worse than an empty slot, and an empty slot
179
+ fails the gate.** The way out is the next section, not a stand-in. (A whine recorded from another
180
+ car's straight-cut race gearbox was rejected for exactly this reason.)
181
+
182
+ ## When the best mod lacks a slot
183
+
184
+ Walk this order for the missing slot only, and record which step you used in its `provenance`:
185
+
186
+ 1. **Another BeamNG mod of the same engine or car family** (a sister trim, the same engine in
187
+ another body), then the **BeamNG base game** (stock FMOD banks, `vehicles/common` sounds,
188
+ stock blends) for anything generic (doors, horn, indicator, tyres, impacts, suspension).
189
+ 2. **Another simulator's mod or official car** (Assetto Corsa / ACC, rFactor 2, Automobilista 2).
190
+ 3. **Real recordings of the car** (onboard and exterior clips). Cut each per "Cutting a real
191
+ recording", below, and name the clip URL in the provenance.
192
+
193
+ Never another car's sound under this car's label, and never a synthesised stand-in. If all three
194
+ fail for a slot the car physically has, the car is **not publishable**: report it blocked and say
195
+ what you tried (an empty `engineLadder` fails the gate for the same reason).
196
+
197
+ The stock events BeamNG itself plays for a slot no part names, the `--map` slot-map file (one car
198
+ from several sources, trims, concats, recordings) and the Assetto Corsa specifics are in
199
+ `read_skill({ name: "helix-vehicles", reference: "audio-import" })`. **`shutdown`, `clutch`, `intake` and
200
+ `revLimiter` have no base-game default**: BeamNG plays nothing for them unless a part names one,
201
+ so they stay empty (and the gate fails) until a `--map` source provides one.
202
+
203
+ ## The only empty slots
204
+
205
+ Two shapes are accepted, and nothing else:
206
+
207
+ ```json
208
+ { "slot": "turboSpool", "buffers": [], "consumed": false, "absent": "not-fitted", "reason": "naturally aspirated: the real car has no turbocharger" }
209
+ { "slot": "exhaustLadder", "buffers": [], "consumed": false, "absent": "combined-with-engine", "reason": "the mod records engine and exhaust as one set; it plays once on the engine layer" }
210
+ ```
211
+
212
+ - **`absent: "not-fitted"`**: a component the real car physically lacks: `turboSpool` and `blowOff` without a
213
+ turbocharger (`physics.induction` absent or not `turbo`), `reverseBeep` (fitted equipment),
214
+ `clutch` on an automatic or CVT, and on an electric car every combustion slot (and the shift
215
+ slots on a single-speed drive). An EV's **engine ladder is a recording of the real motor** and
216
+ `transmissionWhine` stays required; `audio.drivetrain: "electric"` (the engine's synthesised
217
+ motor voice) **fails as `AUDIO_SYNTHESISED`**. See "Electric cars" in the `audio-import` reference.
218
+ - **`combined-with-engine`**: `exhaustLadder` only, beside a **sourced** engine ladder.
219
+
220
+ The reason must name the real component or the mod's combined recording. "No recording found" is
221
+ not a reason: it is `AUDIO_SLOT_UNSOURCED`. A `not-fitted` claim the car's physics contradicts (a
222
+ turbo car's `turboSpool`, a manual's `clutch`), or turbo sounds on a car with no
223
+ `physics.induction`, fails as `AUDIO_SLOT_FITMENT_MISMATCH`.
224
+
225
+ ## Provenance (every slot, every bank)
226
+
227
+ Every slot and bank states where its sound came from. `import_vehicle_audio` writes it; if you
228
+ merge by hand, keep it exactly:
229
+
230
+ ```json
231
+ "provenance": {
232
+ "kind": "sim-mod", // "sim-mod" | "sim-official" | "recording"
233
+ "sim": "beamng", // "beamng" | "assetto-corsa" (a recording: where it was filmed or fetched)
234
+ "mod": "mmbng", // the mod / car folder ("BeamNG.drive" for stock banks) / video title
235
+ "modUrl": "https://…", // when --mod-url or the map's modUrl is given
236
+ "sourceFile": "art/sound/engine/PSH/1768.wav", // or "content/art_sound.zip:art/sound/fmod/desktop/vehicle.assets.bank"
237
+ "sourceSha256": "<sha256 of that source file>",
238
+ "event": "event:/Engine/Starter/v8flat_1986_eng", // the FMOD event, where there is one
239
+ "sample": "<FMOD subsound name>", "subsong": 1234,
240
+ "extractor": "helix beamng_stock_events + vgmstream-cli r2117",
241
+ "transforms": [{ "kind": "wav-header-repair" }], // wav-header-repair | decode | fmod-autopitch-varispeed | trim | concat
242
+ "jbeam": "Porsh_engine.starterSample", // the field that named it (BeamNG)
243
+ "baseGameDefault": "lua/vehicle/…", // when the game's own default filled the slot
244
+ "pieces": [] // the parts of a concat
245
+ }
246
+ ```
247
+
248
+ It lives inline on `audio.slots[i]`, `audio.slots[i].assets[j]`, `audio.bank.assets[k]` and
249
+ `audio.bank.blends[name]`. The only byte changes ever made to a recording are the recorded
250
+ `transforms`: a malformed WAV header rebuilt around untouched samples, a codec decode, an FMOD
251
+ auto-pitch varispeed (ratio recorded), a trim or a concat of real pieces. Nothing is mixed,
252
+ filtered or generated, and `import_vehicle_audio` never alters a recording to pass a gate.
253
+
254
+ `check_vehicle_audio` prints the provenance of every slot (sim, mod, source file, sha256,
255
+ extractor) so you can read the whole chain at a glance. These are **errors**, and `publish_vehicle`
256
+ refuses the same:
257
+
258
+ | Code | When |
259
+ | --- | --- |
260
+ | `AUDIO_SYNTHESISED` | a provenance `kind` or transform that is generated or synthesised, or a slot `reason` / `note` that says so (a negated clause such as "rather than synthesised" is fine) |
261
+ | `AUDIO_ENGINE_UNSOURCED` | no recorded engine ladder: the runtime would play its synthesised fallback engine (electric cars are exempt) |
262
+ | `AUDIO_SLOT_UNSOURCED` | one of the 31 slots is missing, empty (outside the two shapes above) or only names a platform buffer (the runtime's default pack) |
263
+ | `AUDIO_PROVENANCE_MISSING` | a recording (slot asset or ladder rung) that says nothing about where it came from |
264
+ | `AUDIO_MIX_FIELDS_MISSING` | `engineGainDb` / `exhaustGainDb` missing or non-finite, or `minLoadMix` / `maxLoadMix` / `muffling` outside 0..1: the car boards but never moves or sounds (R34 1.0.9). `audio-import` writes all five; `basePackage` keeps a published car's own |
265
+ | `AUDIO_SLOT_FITMENT_MISMATCH` | a `not-fitted` claim the physics contradicts, or turbo sounds on a car with no `physics.induction` |
266
+
267
+ `AUDIO_ENGINE_EXHAUST_SHARED_RECORDING` (warn) is a package that points engine and exhaust blends
268
+ at the same **sourced** recording: a real recording, just doubled; declare the combined shape
269
+ instead. An asset with no traceable source is a defect: replace it.
270
+
271
+ ## Platform defaults are not an answer. Never rely on them.
272
+
273
+ The engine ships its own sound for a slot a car leaves out: a `defaultSoundPack` of generated
274
+ takes (55 of them), a synthetic starter, runtime noise for tyres, skids and the handbrake, and
275
+ synthesised class engine banks (`inline4` and the others in `engine-core`; the multiplayer-vehicles
276
+ template's `craftBanks` are synthesised too). **None of that is a sound for a published car.** A
277
+ car ships its **own sourced sound for every slot**. Do not omit `audio.bank` to "let the platform play something", do
278
+ not name one of those bank ids, and do not treat a quiet gate as "the defaults cover it": the gate
279
+ **fails a car that would fall back to one** (`AUDIO_ENGINE_UNSOURCED` for the engine,
280
+ `AUDIO_SLOT_UNSOURCED` for every other slot).
281
+
282
+ ## Cutting a real recording (fallback step 3, and a one-shot a mod lacks)
283
+
284
+ A steady rung ladder from a real recording of the car, when no sim mod can supply it:
285
+
286
+ - **A steady rpm ladder**: 6-12 rungs from idle to the limiter, each a steady-state stretch at one
287
+ rpm (read it off the tachometer in onboard footage, or from the firing frequency with the
288
+ script above: rpm = f0 x 60 / orders).
289
+ - **On and off throttle**: load and overrun takes at the same rpm, for the load crossfade (row 1
290
+ and row 0).
291
+ - **Turbo cars**: the spool rising under load, and the wastegate or blow-off on lift-off.
292
+ - Ignition, limiter bounce and shifts, if the clip has them.
293
+
294
+ Label every rung by the rpm it **measures**, not by a guess or a tach misread: the runtime places
295
+ a rung on the ladder by its label. Verify every rung with "The rung-pitch self-check", below. Save
296
+ the original audio stream, convert it to PCM WAV and build the rungs as "Preparing WAVs" says.
297
+
298
+ ## The rung-pitch self-check (run it on every rung before sealing)
299
+
300
+ A rung's rpm label is what the runtime uses to place it on the ladder, so the label must describe
301
+ the pitch the rung actually has. Round 4 shipped a top rung labelled `f40_eng_on_7822` (7,822 rpm,
302
+ F40 V8) whose measured fundamental was **21.58 Hz** — the cycle frequency of about **2,600 rpm**, so
303
+ the audio sat at **×0.33 of the rpm its label claimed** and the car lost its redline wail (the
304
+ top-of-range firing read ~104 Hz where the limiter implies ~520 Hz). The cause was a rung labelled by a guess instead of by its
305
+ measured pitch (a sub-order confusion at high rpm); it is mechanical to catch.
306
+
307
+ **The model.** `sampleCylinderCount` (C) and `strokes` (S) describe **the recorded engine** and
308
+ drive the pitch. A rung labelled `N` rpm has three related frequencies, and a comb estimator latches
309
+ onto whichever is strongest:
310
+
311
+ ```
312
+ firing f_fire = N/60 x (C/2 if S == 4 else C) # the dominant bark, the loudest peak
313
+ crank f_crank = N/60 # 1st order, one per revolution
314
+ cycle f_cycle = f_fire / C = N/120 (4-stroke) # waveform repeats once per engine cycle
315
+ ```
316
+
317
+ ### The firing fundamental — which of those three is the car's voice, by crank layout
318
+
319
+ **The firing fundamental is the number you must get right, and it depends on the crank layout as
320
+ well as the cylinder count.** A 4-stroke engine fires each cylinder once per 2 crank revolutions,
321
+ so it fires `C/2` times per revolution. Whether those firings are **evenly spaced** decides which
322
+ frequency is the loudest, and that is the difference between two V8s that otherwise share every
323
+ number:
324
+
325
+ ```
326
+ flat-plane (even-firing) f0 = C x rpm / 120 # evenly spaced: inline, boxer AND flat-plane V8
327
+ V8: f0 = rpm / 15 # 4 evenly spaced firings per rev (the "scream")
328
+ cross-plane V8 (uneven) f0 = rpm / 60 # the crank-order "burble", 2 octaves below
329
+ ```
330
+
331
+ - **Flat-plane** (even-firing — every inline and boxer engine, and the flat-plane V8): the pulses
332
+ are equally spaced, so the energy sits at the **firing frequency** `C·rpm/120`. A flat-plane V8's
333
+ `f0 = rpm/15`: ~65 Hz at a 950 rpm idle, ~516 Hz at the 7,750 rpm limiter. This is the bright,
334
+ high, screaming voice (Ferrari F120A/F40, GT350 Voodoo).
335
+ - **Cross-plane** V8: the crank throws group the four firings unevenly (the classic burble), so the
336
+ dominant is the **crank order** `rpm/60` — two octaves below the flat-plane value, and the
337
+ half-order rumble of an LS or a small-block Chevy.
338
+
339
+ Declare the layout in the package: **`audio.crankLayout`** is `"flat-plane"` (default; every
340
+ inline/boxer engine and the flat-plane V8) or `"cross-plane"` (a cross-plane V8). The default is
341
+ flat-plane because every non-V8 is even-firing and the flat-plane V8 is the one that must be named.
342
+ The gate below uses `crankLayout` to pick which of `{f_fire, f_crank, f_cycle}` the rung's pitch
343
+ must land on, and **rejects a rung an octave or more off** — which is exactly the round-7 failure:
344
+ a flat-plane V8 ladder was authored at the crank order (~138 Hz at the limiter instead of
345
+ ~516 Hz) and the ladder played two octaves low.
346
+
347
+ **Measure the fundamental.** Take each rung's fundamental `f_meas` with a **chance-corrected comb**:
348
+ score every candidate `f0` in about 15–1300 Hz by how many spectral peaks fall near **integer
349
+ multiples** of `f0` and how strong those peaks are, and take the best-scoring `f0`. **A single-lag
350
+ autocorrelation or the single strongest FFT bin is not reliable on real engine audio** — broadband
351
+ noise and non-harmonic orders move it and will mislabel a rung — so do not substitute one for the
352
+ comb.
353
+
354
+ **Normalise the order.** Take `f_meas` relative to the **closest** of `{f_cycle, f_crank, f_fire}`:
355
+ `q = f_meas / closest`. `q ≈ 1` means the rung's pitch matches its label under that order. (Do not
356
+ compare `f_meas` straight to `f_fire`; a rung whose comb landed on the crank or cycle order would
357
+ false-fail.) Record `q` per rung beside its rpm in the ledger.
358
+
359
+ **Pass/fail, on the whole ladder, before sealing:**
360
+
361
+ - **PASS** when every rung's `q` is within **±165 cents (10 %)** of the ladder's **median** `q`, and
362
+ the fundamental **rises monotonically** with the labelled rpm (log–log slope ≈ 1.0). The runtime
363
+ plays rungs by **ratio** of labelled rpm, so a small *consistent* offset across the ladder is only
364
+ a slight absolute-pitch shift — a warning, not a fail.
365
+ - **WARN** (record it, do not ship blind) when the median `q` is 6–25 % from 1 — the labels describe
366
+ the audio only to that tolerance.
367
+ - **FAIL — relabel the rung to the rpm it really is, or re-extract it from the source (a wrong
368
+ rung in the mod's ladder means that mod is not accurate: back to "Which mod is most accurate?") — when:**
369
+ - a rung sits at **half or a third** of the ladder's pitch/label ratio (`q/median ≈ 1/2 or 1/3`).
370
+ This is the round-4 top-rung failure: the audio is really at **2× or 3× the rpm of its label**,
371
+ so it plays a fraction as high as the tach says and the redline never screams. **Never accept
372
+ this as an "order ambiguity" on the top rung** — the top rung is where it costs the most.
373
+ - the median `q` is more than **±386 cents (25 %)** from 1 — the labels do not describe these
374
+ recordings at all.
375
+ - the fundamental does not rise with the labelled rpm.
376
+
377
+ **Controls (measured; reuse them):**
378
+
379
+ ```
380
+ PASS (consistent) a 993 flat-6 ladder (a real BeamNG import) (C 6, S 4), 11 rungs 1768-6388 rpm: q 0.918-0.924,
381
+ median q 0.9187 (-147 cents), spread 0.23 %, log-log slope 0.998 -> the labels
382
+ sit a consistent -8 % from the model convention, which the ratio playback
383
+ absorbs: self-consistent, ships.
384
+ FAIL round-4 F40 f40_eng_on_7822.wav, label 7822 rpm (V8 4-stroke -> f_fire 521 Hz,
385
+ f_cycle 65 Hz): f_meas 21.58 Hz = f_cycle of ~2,600 rpm -> q/median 0.33
386
+ -> relabel, or re-extract.
387
+ ```
388
+
389
+ The 993 control is real: the failure mode to catch is a **single** rung — above all the top one —
390
+ that disagrees with the rest of the ladder, not a consistent offset the whole ladder shares.
391
+
392
+ ## The spectral-centroid check (the car's firing-order signature, not just its pitch)
393
+
394
+ **Rung pitch and timbre are both necessary and neither is sufficient.** Pitch says *what note* the
395
+ engine is on; the **spectral centroid** (the energy-weighted mean frequency, `Σ f·P(f) / Σ P(f)`)
396
+ says *where the energy sits across the harmonics*, and that is what separates a flat-plane V8
397
+ from a cross-plane one. A ladder from the wrong engine passes the pitch check and still does not
398
+ read as the car: one earlier F40 ladder had an idle centroid of **294.7 Hz against the real car's
399
+ 1,779 Hz (×0.17)**, a cross-plane burble where a flat-plane scream belongs.
400
+
401
+ - A **flat-plane** crank (Ferrari F120A/F40, GT350 Voodoo) fires evenly, so the ladder is
402
+ dominated by **high firing orders**: the centroid sits **high** (the real F40: about 1.8 kHz at
403
+ idle, 2.6-3.9 kHz at rev) and its firing fundamental is `rpm/15`.
404
+ - A **cross-plane** crank (LS, SBC) piles energy into the **half-order rumble**: the centroid sits
405
+ **much lower**.
406
+
407
+ **How to use it.** It is the second measure in "Which mod is most accurate?": measure the candidate's
408
+ idle rung and its top rung, and the real car's idle and high-rpm clips, with `measure-engine.py`.
409
+ **PASS** when the candidate's centroid is within **±30 %** of the real car at idle and at the top
410
+ and **rises with rpm**. A candidate that fails is a different engine in that car's body: **choose
411
+ another mod, or another BeamNG mod of the same engine; do not EQ it into shape, and never
412
+ resynthesise it.** `check_vehicle_audio` gates pitch (against the firing fundamental, above) and
413
+ broadband content but, unless you pass it a real-car reference, not the centroid: a green gate is
414
+ not "the voice is the car's", so verify the centroid against the real recordings yourself.
415
+
416
+ ## Preparing WAVs
417
+
418
+ - Use PCM WAV, 44.1 or 48 kHz, 16-bit, and keep one sample rate across the bank. `audio-import`
419
+ writes this for you; for a recording you cut yourself: `ffmpeg -i in.m4a -ac 1 -ar 48000 -c:a pcm_s16le out.wav`.
420
+ - Ladder rungs must be steady-state loops. Trim each to a whole number of firing cycles and remove
421
+ any fades. Name files by rpm, as in `psh_3214.wav`.
422
+ - Normalise the ladder rungs to each other, then set the overall level with `engineGainDb` and
423
+ `exhaustGainDb` (the 993 used −8 and −5). `muffling` 0.22 and `minLoadMix` 0.15 are reasonable
424
+ starting points. **Normalise with headroom** (peak −3 dBFS or lower) and never let a limiter or a
425
+ `tanh` drive flatten the peaks: see "Content gates", below.
426
+ - `sampleCylinderCount` and `strokes` describe **the recorded engine**, and `crankLayout`
427
+ (`"flat-plane"` or `"cross-plane"`) names the crank. They drive the pitch model — and they are the
428
+ inputs to "The rung-pitch self-check" and the firing fundamental, above, before any rung is sealed.
429
+
430
+ ## Wiring it in the package
431
+
432
+ ```json
433
+ "audio": {
434
+ "bankId": "<slug>", "sampleCylinderCount": 6, "strokes": 4, "crankLayout": "flat-plane",
435
+ "engineBlend": "MYENGINE", "exhaustBlend": "MYEXHAUST",
436
+ "bank": {
437
+ "blends": {
438
+ "MYENGINE": { "eventName": "event:>Engine>default",
439
+ "samples": [[["r1768.wav", 1768], ["r2238.wav", 2238], "…"], [["r1768.wav", 1768], "…"]] },
440
+ "MYEXHAUST": { "eventName": "event:>Exhaust>default",
441
+ "samples": [[["x1800.wav", 1800], ["x2300.wav", 2300], "…"]] }
442
+ },
443
+ "assets": [{ "name": "r1768.wav", "file": "r1768.wav", "role": "vehicle.audio.bank.sample",
444
+ "mediaType": "audio/wav", "bytes": 356236, "checksumSha256": "<sha256>",
445
+ "cid": "cid:sha256:<sha256>", "url": "<public URL of the sealed object>" }]
446
+ },
447
+ "slots": [
448
+ { "slot": "engineLadder", "buffers": ["blend:MYENGINE"], "consumed": true,
449
+ "provenance": { "kind": "sim-mod", "sim": "beamng", "mod": "…", "modUrl": "https://…",
450
+ "sourceFile": "art/sound/blends/….sfxBlend2D.json", "sourceSha256": "<sha256>", "extractor": "helix vehicle audio-import <version>" } },
451
+ { "slot": "exhaustLadder", "buffers": ["blend:MYEXHAUST"], "consumed": true, "provenance": { "…": "as above" } },
452
+ { "slot": "startup", "buffers": ["startup.wav"], "assets": [{ "…": "same asset shape" }], "consumed": true, "provenance": { "…": "as above" } },
453
+ { "slot": "turboSpool", "buffers": [], "consumed": false, "reason": "naturally aspirated" }
454
+ ],
455
+ "engineGainDb": -8, "exhaustGainDb": -5, "minLoadMix": 0.15, "maxLoadMix": 1, "muffling": 0.22
456
+ }
457
+ ```
458
+
459
+ Rules:
460
+
461
+ - Every sample a selected blend names must be in `bank.assets`.
462
+ - A slot's `assets` must be referenced by that slot's `buffers`.
463
+ - No slot may be declared twice.
464
+ - `audio.bank` and every consumed slot carry a `provenance` ("Provenance", above).
465
+
466
+ ## The URL closure and package closure (silent failure if skipped)
467
+
468
+ **Every audio file must be a member of the sealed package closure.** A WAV a bank or slot names has
469
+ to be listed in the package (`bank.assets`, or the slot's `assets`) and sealed by `publish_vehicle`.
470
+ A slot that references a `cid` which is not inside the closure resolves to nothing and **404s at
471
+ runtime** — silently.
472
+
473
+ The runtime loads that sealed asset only when it carries a `url`, its `checksumSha256` and its
474
+ `bytes`, and the URL serves exactly those bytes. **A `cid` alone plays nothing**, and nothing
475
+ reports it.
476
+
477
+ Do not compute URLs. Author each row with `name` and `file` (the WAV in the package directory or
478
+ `audio/`), as in the example above, and let `publish_vehicle` close it: it uploads each WAV as a
479
+ public object, confirms its content URL serves the exact bytes, writes `url`, `checksumSha256`,
480
+ `bytes` and `cid` before the mint, and includes the file in the sealed closure. Every sample, a
481
+ mod's one-shot included, is sealed the same way: the WAV from `audio-import` sits beside the
482
+ package and `publish_vehicle` uploads and seals it. A `url` copied from the reference package's
483
+ `bg_*` rows is not in your closure and 404s (and those rows are another car's sound).
484
+
485
+ `check_vehicle_audio` is the proof, before and after:
486
+
487
+ ```
488
+ check_vehicle_audio({ target: "/abs/car" }) # pre-publish: local files + any existing urls
489
+ check_vehicle_audio({ target: "<vehicle item id>" }) # read-back: what the runtime receives
490
+ ```
491
+
492
+ It selects samples exactly as the runtime does (each rung of the selected blends, each slot asset
493
+ named by its slot's `buffers`), then fetches and decodes each one. It fails a missing row, a 404,
494
+ a byte or digest mismatch, a non-PCM file and digital silence, and it prints the **provenance** of
495
+ every slot and bank; it **refuses synthesised or generated audio** (`AUDIO_SYNTHESISED`) and a car
496
+ whose engine or exhaust has no sourced audio (`AUDIO_ENGINE_UNSOURCED`). Three further gates are **errors**:
497
+
498
+ - **Each ladder rung's pitch must match its rpm label against the firing fundamental.** Every engine
499
+ and exhaust rung is decoded, its fundamental measured with a chance-corrected comb, and compared
500
+ to the firing fundamental for the declared `sampleCylinderCount`/`strokes`/`crankLayout`
501
+ (`C·rpm/120` even-firing, `rpm/15` for a flat-plane V8, `rpm/60` for a cross-plane V8). A rung
502
+ **an octave or more off** from that frequency is refused — this is the round-7 failure, where the
503
+ flat-plane V8 ladders were authored at the crank order (~138 Hz at the limiter instead of
504
+ ~516 Hz) and played two octaves low. A correct ladder passes; an octave-low one fails. The
505
+ octave check (`AUDIO_RUNG_PITCH_OCTAVE_OFF`) decides on the spectral energy at the labelled
506
+ firing series (or its half-order series, which real flat-plane V8 and flat-six recordings
507
+ carry) before it trusts the pitch tracker, so real sim recordings pass.
508
+ - **Engine and exhaust are separate recordings, unless the mod's is one combined set.** Pointing
509
+ `engineBlend` and `exhaustBlend` at one unsourced blend is refused
510
+ (`AUDIO_ENGINE_EXHAUST_SAME_BLEND`). A mod that records one set for both omits `exhaustBlend` and
511
+ declares `exhaustLadder` `absent: "combined-with-engine"`; pointing both blends at the same
512
+ *sourced* recording is only a warning (`AUDIO_ENGINE_EXHAUST_SHARED_RECORDING`).
513
+ - **A rung must be a real broadband engine sound, not a near-pure tone.** Each engine and
514
+ exhaust rung is measured for spectral richness (autocorrelation and the spectral noise floor). A
515
+ near-pure tone — a handful of spectral lines, everything else down, autocorrelation ~1.0 — is
516
+ refused even when its pitch is correct (round 5 shipped an 11-rung ladder whose 1000 rpm rung was
517
+ exactly that). A recorded rung, with its real harmonic ladder and noise floor, passes.
518
+
519
+ ## Content gates (clipping, DC, copies, fixed formants, synthetic horn)
520
+
521
+ `check_vehicle_audio` also measures what the sound is made of. Round 8's F40 passed every gate above
522
+ and an owner listening to it still heard these, so each is now measured. Thresholds are calibrated
523
+ so clean recordings pass (the 993, BeamNG's F355, the platform's recorded horns and 41 field
524
+ recordings all pass untouched). A **warning** is reported, not blocking; an **error** blocks the mint.
525
+
526
+ | Code | Means | Level | Fix |
527
+ | --- | --- | --- | --- |
528
+ | `AUDIO_CLIPPED` | Flat-topped runs (3+ identical samples with a steep knee) at full scale or at the file's own peak: clipped, then turned down. Real recordings: ≤ 0.04 % of samples. | warn from 0.1 %; error from 0.5 % on a ladder rung, 2 % on any other sample | Render with headroom (peak ≤ −3 dBFS); remove the hard limiter or `tanh` drive that flattens the crest, or lower its drive. Turning a clipped file down does not unclip it. |
529
+ | `AUDIO_DC_OFFSET` | Mean sample value not zero (real: ≤ 0.015 full scale); the sample clicks when it starts or loops. | warn from 0.02; error from 0.05, or 50 % of RMS | Subtract the mean after the last nonlinearity, or high-pass at 25 Hz (`ffmpeg -af highpass=f=25`). Asymmetric waveshapers and limiters cause it. |
530
+ | `AUDIO_LADDER_FIXED_FORMANTS` | Every rung has the same peaks and notches at the same absolute Hz, whatever the rpm: one resonator stack stamped on every rung (harmonic-envelope correlation across rungs ≥ 0.3; real ladders ≈ 0.0–0.1, the F40 0.64–0.73). | warn ≥ 0.3; error ≥ 0.5 | One resonator stack stamped on every rung is the signature of a generated ladder, and a refusal here is a sign the rungs did not come from a real engine. Take the ladder from the sim mod's own recordings (or another mod of the same engine), where the resonances move with rpm and load. |
531
+ | `AUDIO_LADDER_FIXED_TONE` | A narrowband line at the same absolute frequency in most rungs while the firing fundamental moves (a fixed oscillator or ring); lines that are harmonics of the rung's own pitch are ignored. | warn when in ≥ 55 % of rungs; error ≥ 75 % | Remove or notch the fixed component. Only broad body resonances may stay put. |
532
+ | `AUDIO_LADDER_RUNG_COPY` | A rung is the neighbouring rung time-stretched or resampled (aperiodic residual correlates ≥ 0.65 after undoing the pitch change; independent rungs ≤ 0.5). | warn ≥ 0.65; error ≥ 0.8 | Do not resample one rung into its neighbours. Use every rung from its own recorded take (own noise, own cycle-to-cycle variation); the runtime already pitch-shifts between rungs. |
533
+ | `AUDIO_HORN_SYNTHETIC` | The `horn` slot repeats sample for sample (autocorrelation ≥ 0.9995: no pitch jitter, shimmer or noise floor); recorded horns measure 0.97–0.98. | warn | Use the car's recorded horn (the mod's horn event, then a real recording). A horn that repeats sample for sample was generated; replace it. |
534
+
535
+ Two traps: a ladder whose rungs share one resonator set fails the fixed-formant gate even though
536
+ no rung is a copy of another, which is the usual way a generated ladder ends up sounding like one
537
+ voice at different pitches. And a **real BeamNG import**
538
+ (993-style) may declare a bad WAV header (two channels, mono block alignment): the content gates
539
+ decode it leniently and measure it normally.
540
+
541
+ A real recording can still trip the content gates (Assetto Corsa's own exterior sets clip and carry
542
+ DC offset). `import_vehicle_audio` never alters a recording to pass a gate; take another take or
543
+ another source for that rung (the same car's other microphone set, another mod), never a processed
544
+ or synthesised one.
545
+
546
+ ## Source gates: distinct recordings, the limiter, the real-car reference
547
+
548
+ Every sound is a **real recording** of the car (or of a BeamNG mod of it). `check_vehicle_audio` also refuses sounds that are not distinct, recordings that are the wrong length for how the runtime plays them, and, when you give it a recording of the real car, an idle voice that does not sound like it.
549
+
550
+ | Code | Means | Level | Fix |
551
+ | --- | --- | --- | --- |
552
+ | `AUDIO_NEAR_DUPLICATE_SOUNDS` | Two sounds that play as different roles (shift up / shift down, horn / an engine rung, an engine rung / an exhaust rung, door open / close, any two slots) are one recording: identical samples, or one retimed or pitched into the other (band-envelope correlation ≥ 0.93, or ≥ 0.8 with the same resonance line pattern; independent recordings ≤ 0.66). Inside one ladder the finding is `AUDIO_NEAR_DUPLICATE_RUNGS`. | error; `AUDIO_POSSIBLE_DUPLICATE_SOUNDS` (warn) for a 0.8-0.93 match the line pattern does not back | Use a distinct real recording for each role. A shift-down is not the shift-up retimed; an exhaust rung is not an engine rung resampled. A BeamNG mod of the car usually ships separate ones. Listen to a warned pair before accepting it. |
553
+ | `AUDIO_REV_LIMITER_HELD` | The `revLimiter` sample is longer than the runtime plays it: the runtime re-fires the slot every 60 ms while the car sits on its limiter, so a 1.6 s recording puts about 27 copies in the air at once and plays as a held tone. The fuel-cut stutter **is** the retrigger. | warn > 0.2 s; error > 0.35 s | Use a real recording of this car cut down to one fuel-cut crack of 0.05-0.2 s with its natural decay; do not ship a long recording of the car held on its limiter. |
554
+
555
+ ### Compare with the real car (`reference`)
556
+
557
+ ```
558
+ check_vehicle_audio({ target: "/abs/car", reference: ["/abs/real-idle-1.wav", "/abs/real-idle-2.wav"] })
559
+ check_vehicle_audio({ target: "/abs/car", reference: ["horn=/abs/real-horn.wav", "startup=/abs/real-start.mp3"] })
560
+ ```
561
+
562
+ - A bare path is a recording of the real engine **idling**; it is compared with the `idle` slot sample, else the lowest engine-ladder rung. `slot=path` compares that slot's sample instead. Repeat to give several clips: the median is used. WAV directly; mp3, m4a and ogg need `ffmpeg` on the PATH (or convert: `ffmpeg -i in.mp3 -ac 1 -ar 44100 -c:a pcm_s16le ref.wav`).
563
+ - Use 10-30 s of **steady** idle, close to the car, with no speech or music. The same two measures are taken from your sample and the reference with one implementation: **spectral centroid** (how bright) and **roughness** (depth of the 8-60 Hz firing modulation). `AUDIO_REFERENCE_TIMBRE` is a **warning** beyond x2.5 and an **error** beyond x10. Two honest field recordings of one car differ by x2-4 (mic distance, wind, the recorder's EQ), so read a warning as "listen and compare spectrograms"; an error means a different engine or a heavily processed file.
564
+ - Far duller or smoother than the reference: the sample is from a different engine, low-passed or heavily processed. Far brighter or rougher: a different engine, or a recording with heavy added noise or an exaggerated chug. Re-source it from a real recording of the car.
565
+
566
+ ## Verifying
567
+
568
+ Visual QA never plays audio, so verification is on you. Drive the live car in a **platform-hosted
569
+ world that has vehicle audio**. A world with no `@helix/engine-core/audio` entry in its import map
570
+ plays nothing for any car. Listen for:
571
+
572
+ - ignition;
573
+ - the idle ladder;
574
+ - a smooth rev sweep under load, with no stepping between rungs;
575
+ - shifts;
576
+ - the limiter;
577
+ - the horn;
578
+ - tyre sounds under wheelspin.
579
+
580
+ A missing asset URL shows up as silence, not as an error.