@hypersoniclabs/helix-mcp 0.2.4 → 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.
- package/README.md +81 -11
- package/dist/continuumCanary.d.ts +17 -0
- package/dist/continuumCanary.js +17 -0
- package/dist/continuumCanary.js.map +1 -0
- package/dist/server.d.ts +14 -1
- package/dist/server.js +4058 -162
- package/dist/server.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/vehicleTools.d.ts +86 -0
- package/dist/vehicleTools.js +229 -0
- package/dist/vehicleTools.js.map +1 -0
- package/docs/avatar-face.md +115 -0
- package/docs/bridge.md +98 -0
- package/docs/bring-your-world.md +117 -0
- package/docs/catalog.md +69 -1
- package/docs/character-animation.md +442 -0
- package/docs/character-attachments.md +166 -0
- package/docs/character-world.md +785 -130
- package/docs/continuum.md +153 -0
- package/docs/items.md +73 -0
- package/docs/lighting-world.md +667 -0
- package/docs/locomotion-clip-spec.md +294 -0
- package/docs/manifest.md +31 -6
- package/docs/multiplayer-logic.md +460 -17
- package/docs/multiplayer-templates/chrono-orchard.md +36 -22
- package/docs/multiplayer-templates/collect-a-thon.md +28 -38
- package/docs/multiplayer-templates/collections.md +24 -24
- package/docs/multiplayer-templates/hangout.md +124 -111
- package/docs/multiplayer-templates/npc-wave.md +310 -0
- package/docs/multiplayer-templates/obby.md +13 -16
- package/docs/multiplayer-templates/persistent-progress.md +218 -0
- package/docs/multiplayer-templates/physics-bumper.md +27 -9
- package/docs/multiplayer-templates/physics-football.md +22 -8
- package/docs/multiplayer-templates/relic-bearers.md +12 -15
- package/docs/multiplayer-templates/server-motion.md +16 -19
- package/docs/multiplayer-templates/shooter-range.md +275 -0
- package/docs/multiplayer-templates/team-control.md +28 -15
- package/docs/multiplayer-templates/turn-arena.md +31 -22
- package/docs/multiplayer-templates/voice-radio.md +166 -0
- package/docs/multiplayer-templates/wave-survival.md +7 -8
- package/docs/multiplayer-templates/world-shop.md +240 -0
- package/docs/multiplayer-world.md +222 -129
- package/docs/npc-world.md +623 -0
- package/docs/publishing.md +108 -28
- package/docs/purchases.md +223 -0
- package/docs/scene-performance.md +64 -0
- package/docs/screenshots.md +140 -0
- package/docs/sdk.md +324 -5
- package/docs/shooter-worlds.md +537 -0
- package/docs/terrain.md +173 -0
- package/docs/upgrades.md +324 -0
- package/docs/vehicles.md +727 -0
- package/docs/world-inspect.md +156 -0
- package/docs/world-look.md +241 -0
- package/docs/world-recipe.md +65 -6
- package/package.json +15 -4
- package/skills/README.md +91 -0
- package/skills/helix-assets/SKILL.md +491 -0
- package/skills/helix-assets/references/asset-sources.md +143 -0
- package/skills/helix-assets/references/vault-api.md +105 -0
- package/skills/helix-avatar-qa/SKILL.md +85 -0
- package/skills/helix-avatars/SKILL.md +206 -0
- package/skills/helix-avatars/references/contract.md +166 -0
- package/skills/helix-avatars/references/dynamics.md +367 -0
- package/skills/helix-avatars/references/face.md +50 -0
- package/skills/helix-avatars/references/publish.md +76 -0
- package/skills/helix-avatars/references/qa.md +251 -0
- package/skills/helix-avatars/references/rigging.md +88 -0
- package/skills/helix-avatars/references/source-generated.md +190 -0
- package/skills/helix-avatars/references/source-model.md +90 -0
- package/skills/helix-avatars/references/source-rigid.md +90 -0
- package/skills/helix-avatars/references/source-vrm.md +61 -0
- package/skills/helix-gauntlet/SKILL.md +128 -0
- package/skills/helix-multiplayer/SKILL.md +150 -0
- package/skills/helix-multiplayer/references/dsl-capability-map.md +107 -0
- package/skills/helix-vehicles/SKILL.md +218 -0
- package/skills/helix-vehicles/references/addons.md +212 -0
- package/skills/helix-vehicles/references/appearance.md +339 -0
- package/skills/helix-vehicles/references/audio-import.md +138 -0
- package/skills/helix-vehicles/references/audio.md +580 -0
- package/skills/helix-vehicles/references/cabin.md +225 -0
- package/skills/helix-vehicles/references/host-manifest.md +174 -0
- package/skills/helix-vehicles/references/physics-from-specs.md +463 -0
- package/skills/helix-vehicles/references/publish.md +214 -0
- package/skills/helix-vehicles/references/qa.md +177 -0
- package/skills/helix-vehicles/references/reference-package.json +3481 -0
- package/skills/helix-vehicles/references/reference-package.md +69 -0
- package/skills/helix-vehicles/references/source-beamng.md +167 -0
- package/skills/helix-vehicles/references/source-concept.md +38 -0
- package/skills/helix-vehicles/references/source-model.md +100 -0
- package/skills/helix-vehicles/references/source-scratch.md +60 -0
- package/skills/helix-world-build/SKILL.md +376 -0
- package/skills/helix-world-build/references/config-gates.md +104 -0
- package/skills/helix-world-director/SKILL.md +210 -0
- package/skills/helix-world-qa/SKILL.md +371 -0
- package/skills/helix-world-qa/references/perf-budgets.md +240 -0
- package/skills/helix-world-qa/references/perf-handle.md +125 -0
- 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.
|