Library::surface_map and PrefetchPool::contains are only read by tests; mark them
cfg(test) (the manifest field stays, read by the vault check). No behaviour change.
The audible autotest used one beep for eight steps, so steps couldn't be told apart.
Each step now has its own self-made sound (synthesize.py): bell (map Baby_Cry_1), chime
(F6 post, Buoy_Bell patch 88), wood block (F8), triangle (F10), low horn (F11 landing),
click (pop rule), whistle (Digit5), shaker (Digit6), boing (honk rule). HUD: one compact
box top-left, every line fits one row, live countdown; camera return after step 3.
Six identical taxis at one rpm and level, each detuned once by C04's own Random op,
beat at 4-8 Hz (the "lawn mower" heard in the audible autotest). Each taxi now drives its
own ring (radius 3+3i m, 2+2i m/s), headless engine-band modulation at single-taxi level.
mod_rules: Tags is now Copy with fixed arrays, so the rule-set key is the Tags value and
the tests build arrays. No-mod e2e bench identical to m2 (84 / 84); tests pass.
mods/audio-content-test runs every action on a timer once the map's native audio runs
(settings autotest, autotest_step, autotest_gap, autotest_muted) and logs one
AUTOTEST <check> ok|fail line per read-back check; one bottom-left HUD box with the step,
what to listen for and a red MUTED TEST header on muted runs. Doc 16 M8b.
A mod subscribing in on_load (before native audio runs) kept empty pop / land tags all
session. events_frame now copies the player's pop / land ids on every subscribed frame
(Copy, no allocation; nothing runs without a subscriber); id 0 never tags pop / land.
Found by the in-game autotest. Test, doc 16 entry; no-mod e2e bench identical (84 / 84).
- sdk.world_audio.spawn(key, 'traffic'|'ped', ...) without slots gets its own instance on
the private MixMap, as mod emitters do; slots = 'shared' (alias 'retail') opts into
retail's pools. Capability world_audio 4. Engine-published objects are unchanged.
- Full private pool (16 cars + 16 peds, all mods): the nearest play, the rest in reach wait
silently; never refused, never in retail's pools. read(key).waiting / .slots,
info().own, a one-time log line.
- Dev mods: audio-content-test uses the default; world-audio-test asks for 'shared'
(retail's limits) with an "Own instances" setting.
- No mod active: e2e bench byte-identical (84 / 84); suites pass (known failures only).
Completes the "Later" list (doc 16 section M):
- Hot swap: an audio mod turning on / off is swapped into the running audio in place.
Banks keep their runtime id and constructor order, held posts are re-bound with their
payload; Splice trees, wheel streams, projects, tuning and speech swap too. Only a
MixMap or grain-bed change (or a swap error) still restarts the sound.
sdk.audio.info().swaps / last_change.
- Hot reload: editing audio.json or the files it names swaps the overlay while the
script keeps running.
- MixMap inputs: sdk.audio.set_mixmap_input / mixmap_inputs drive retail's own
controllers (validated, owned, 16 per mod / 64 total, released values restored).
- Own instances: world_audio spawn {slots='own'} plays mod cars / peds on a private
MixMap (16 + 16), retail pools untouched.
- Csis projects: audio.json add.projects ships a mod's own classes, functions and
globals; name clashes are refused; installed / removed without a restart.
- Seed: sdk.audio.seed / SKATE_AUDIO_SEED seeds all audio generators.
- Rules: play.frame = 'owner' puts offsets in the owner's axes; rule sounds follow
moving published emitters.
- Capabilities: audio 4, audio_content 3, audio_events 3, world_audio 3.
- Dev test mod: Digit1-6, a second HUD line, its own self-made dev.csi.
- No mod active: e2e bench byte-identical 84 / 84 after every step.
- Mod emitters get their own instances by default (ModEmitterSlots::Extra); "shared" stays
selectable, SKATE_AUDIO_MOD_EMITTER_SLOTS overrides either way.
- Mod WAVs route through the native mixer by default when the native audio runs and a voice
is free, else the Bevy path; native = true / false forces one. Positional native sounds
default to a 40 m squared reach.
- Replace / layer rule sounds are positional: `at` = owner (default, followed each frame,
optional offset), world (fixed position) or centre; optional falloff; default reach is the
owner's retail one (emitter record, cars 40 m, peds 50 m, skaters 30 m).
- Dev test mod, Skyline and audio-example use the defaults; SDK docs and doc 16 updated
(section L questions written as decisions).
- No mod active: e2e bench byte-identical, 84 / 84; new tests for each default.
- Mod WAVs can play through the game's own mixer (per-mod mixer banks, retail emitter
words, a private MixMap), beside the existing Bevy path (mod_voices.rs).
- Tuning can change while the game runs: sdk.audio.set_tuning / tuning / tuned, applied
between passes, exact restore on nil or mod disable (tuning.rs, audio_tuning.rs).
- Mods add world emitters and reverb zones through the published WorldEmitter /
ReverbZoneVolume components (world_audio kinds).
- Sound rules: mods mute, replace or layer the game's own sounds by identity, from
audio.json `rules` or sdk.audio.rule (mod_rules.rs, audio_rules.rs); empty or
rules-only overlays no longer restart the sound.
- Capabilities: audio 3, audio_tuning 1, audio_events 2, audio_content 2, world_audio 2.
- Dev test mod: F8 native voices, F9 tuning, F10 emitter + reverb zone, F11 a mute rule.
- Headless e2e bench byte-identical to the #36 head after every step; doc 16 section L.
- Mod crossfade banks play: crossfade layouts come from the bank's program or from
`add.crossfade_layouts` (crossfade.rs, crossfade_layouts.rs); dev test mod fade sample.
- Speech clips and takes are checked at merge time and by check_mod.
- Empty Lua tables (`{}`) were parsed as maps and rejected by list fields; one helper
(lua_list.rs) now reads them as empty lists in every list field of every mod command.
- Traffic lights answered in the world-audio spec (Skate 3 has none).
- Doc 16 and PULL-REQUESTS.md updated.
- data-gated test: main-cast takes, ped one-shot tuning (body-fall ids,
tazer time), the speech echo delay and the Tazer bank's samples come
from an overlay;
- the world event-tag test also drives a tazing, falling ped (tags
"tazer" and "body_fall");
- doc 16: speech archives (livingworld / maincast), world tuning examples,
new tags and event rows, verification after the merge; PULL-REQUESTS row M.
Brings optimisation pass 2, the tools split (#37) and the world gaps
(tazer, ped body falls, speech echo / voice details, main-cast speech,
NPC loose-board slide) under the audio modding layer:
- main-cast speech takes go through the content overlay (archive
"maincast"), like the living world's;
- ped speech (phone ring), body-fall and foot-plant Splice starts and the
NPC skaters' starts are recorded through the observe-only event wrapper;
tazer posts get a slot name; new tags "tazer" and "body_fall"; main-cast
line starts are speech rows with class "maincast";
- the new world tuning (ped_objects, speech_voice) is overlay-reachable
through tuning.world like the rest of the world tuning.
Tazer and PedBodyFall ported from the retail mechanism and checked against the recordings; ped speech gets its PEAK filter, the voice float, the speech inputs, the phone and photographer lines, first-free-stream choice and the queue clock; the speech echo send (pre-gain send -> high pass -> distance delay -> low pass -> environment bus); the main-cast speech channel for pros, NPC skaters' own speech and message pairs, with its opt-in setup decode; the NPC skater's board slide. Engine and mod surfaces extended (reactions, tazer, body fall, phone / photo events); retail values are setup-data defaults. Local player output byte-identical.
regression-checks, setup-equivalence, collision-inspect, world-stream-inspect, vault-inspect and recomp-code-search now live on tooling/published-tools (#37); the audio tools stay here. Index, doc 14 and the PR plan point there.
- AUDIO_TIMING logs voices / instances / grains per block (tools/audio-bench summary prints them).
- The render path is allocation-free in real play: subscriber lists and constructor lists read in place, call
parameters on the stack, instance memory and client lists from pools (render allocations per block 0.17-0.63 ->
<= 0.001; full world 1.825 -> 0.004); render_alloc tests cover walks, posts, calls, globals and a retail world host.
- Voices: direct PCM reads when a block provably stays inside the sample; shared scratch planes in the mixer
(full world render p50 -11 to -14 %).
- MixMap keeps its held-input list between ticks; Jitter processes without a per-frame Vec.
- Every change byte-identical in all 84 e2e outputs; two attempted changes dropped (no real gain).
- Ped speech plays: speech manager -> library -> the living world's two streams, with queue / interrupts and the
near / far lines by distance; levels and filters from the retail mechanism (filters equal to the recomp's, gain
median within 0.5 %). Silent and logged when the opt-in speech decode is absent.
- NPC skater: wheels and clothing on its own instance and a bail grunt; tricks, treatment and footsteps stay off for
NPCs as in retail.
- Per-model ped data (shoe class, voice, far threshold) and traffic engine records per model exported by setup;
traffic relative-speed input and front / rear sound points; world hosts process before the mixer tick.
- Test mod: opt-in (off by default), higher world-object limits (48 per mod, 128 total), a per-second summary log.
- Specs published in docs/hails-additions/audio-specs/; code comments point at them; test data paths behind env vars
(no .local defaults).
The plan for making the native audio moddable: a content overlay that replaces or adds banks, samples, speech and emitters by retail identity; map audio as data so custom maps get emitters and ambience; posting retail sounds and globals from mods; observe-only audio events; example mods. Retail stays byte-identical with no mod active.
Review fixes:
- The audio host takes each physics step exactly once (one-step pulses latched, nothing while paused or silenced,
at most 4 steps per frame), so pushes and other one-frame events are never dropped or doubled. e2e at 60 fps and
above is byte-identical; 30 / 45 fps change only by the latch and the step pair.
- Removed the emitter PROFILES fallback, the stray trace dump and the audition script; data-gated tests are
#[ignore = "needs the private install data"] and fail loudly when run without it; per-post log lines behind
SKATE_AUDIO_TRACE; checked bank parsing, finite output, camera-cut Doppler reset, fewer render allocations.
- The 15 verdict-settled A/B switches are removed; their "on" behaviour is the only one (byte-identical).
World / NPC audio, hookable by engine systems and mods:
- Map-change fix: world and NPC hosts reset when the map's banks are unloaded, so surviving objects sound again.
- Engine surface (skate-game::world_audio): TrafficAudio / PedAudio / NpcSkaterAudio components, horn / alarm /
speech messages, read-back of who is audible; retail limits (4 cars within 40 m, 15 peds within 50 m with
footsteps for the nearest 3, one NPC skater within 30 m), measured in the recompilation.
- Mod surface (Lua API 2, `sdk.world_audio.*`); test mod `mods/world-audio-test/` (cars, peds, a ghost skater).
- Per-skater audio state builder (local player output identical); remote players take the NPC skater slot; an
opt-in non-retail "more audible" setting.
- Docs: doc 15 "Hooking up world audio", doc 11 sections, PR plan.
- tools/recomp-trace: bail_impacts.py (per-bail ragdoll impacts and sound posts) and collision_posts.py (collision-sound posts by poster and owner, with the local-rider flag); README entries.
- Doc 11: listening verdicts, the riding body-collision posts are NPC skaters' (local only on big-drop landings and bumps).
- Doc 13: playing the recomp from the couch and closing it (settings overlay, LB+RB+Back hold).
- Doc 14: the added tools.
- Retail steps the board's turn intensity (0.06) and brake (0.05) once per SkateBoard process call, with no dt,
so on the 30 fps console a full turn fades over 333 ms and the brake over 667 ms. Ours stepped per 1/60 s tick,
twice as fast. Both now step once per console frame, frame-rate independent (identical at 30-365 fps); the NPC
bed follows the same cadence. SKATE_AEMS_SLEW_CONSOLE=0 = old (byte-identical renders).
- Concrete ledges grinded as metal: retail stores every audio material as the surface tag - 1 (as our wheels,
deck and feet already did), but the grind material used the raw tag and read the next AudioSurfaceMap entry
(University ledge tag 66 -> metal). Now material_of_tag like the rest; matches the recomp's grind starts
(material 65, concrete GRINDS samples, no Skate_Metal) at the same spots.
- Docs: doc 11 sections for both, gotchas.
- Body region impacts are scaled by retail's centre-of-mass speed curve (x1 at rest up to x5 from ~0.95 m/s,
read from the vault; no clamp after the multiply), so bails reach the tier-1 / tier-2 body and concrete hits.
Setup exports the curve; older installs get the identical default. SKATE_AEMS_BODY_CURVE=0 = old.
- Region impacts take |Δv·n| as retail does (matches 12,054 of 12,054 recomp region lines).
- The body and deck posters' cooldowns count console frames (0.5 s / 0.2 s at any frame rate).
SKATE_AEMS_BODY_CONSOLE=0 / SKATE_AEMS_DECK_CONSOLE=0 = old.
- Second grain bed for the NPC skater (retail's per-instance SkateBoard update; inert until the engine has
NPC skaters). The local player's renders are byte-identical.
- The state log records the COM speed; the e2e harness replays it.
- Docs: doc 11 (bail density: the missing speed curve, not the ragdoll; the hooks' local test), PR plan,
docs index; the published recomp trace reader knows the new line kinds.
- The interim cue tables and their opt-outs are gone; the native runtime is the only audio path. Old
settings/audio.json files still load ("interim" / "native" ignored). Missing native data logs an error
instead of falling back.
- Water splash from retail's mechanism (SFXObj_OffBoard start / update): Skate_Collisions 1187 / 1197 once
per water stay, 1198 when the board is in water.
- The random interim riding bed is removed; riding collision and cloth sounds compared with the recomp
(doc 11). Native output otherwise byte-identical (13 scenarios + 5 sessions).
- Published tools under tools/ (one folder per tool with a README; the recomp ones labelled for use with the
research hooks), doc 14 and PR plan bucket L.
- Docs: doc 11 (retirement, bail density measured against the recomp, collision posts), doc 13 (usage guide).
- Native AEMS runtime is the default (`SKATE_AEMS=0` or `"interim": true` for the interim tables, kept).
- MixMap, eEQChain clear and Jitter on the console's 30 Hz cadence, frame-rate independent.
- Optimisation pass (render p50 210 -> 130 us, 0 allocations per block, byte-identical) and hardware FMA
dispatch in the new skate-audio-fma crate (p50 -> ~55 us, byte-identical; skate-audio stays forbid(unsafe)).
- World emitter banks decoded ahead on a worker thread (no game-thread decode spikes).
- Session-review fixes: push sounds at the foot plant, push-foot plant/lift, bail body impacts, grind on/off.
- Foundations, inert until the engine has the systems: traffic and ped sounds, ped speech rules, NPC skaters'
board sounds.
- Docs: doc 11 sections, doc 13 (the recompilation research hooks, reference only), PR plan.
Native port of Skate 3's retail audio runtime (AEMS) behind SKATE_AEMS=1, plus the world audio work:
- skate-audio crate: formats, patch-program evaluator, voice graph, mixer, MixMap, granular rolling bed and its
bus chain, Splice contacts, the buses (env reverb network, eEQChain, flanger returns, collision and footstep
submixes, reverb zones) and the player components (rolling layers, seams on the console's 30 fps cadence,
grinds, skids, squeaks, tricks, treatment, footsteps, clothing, hands on deck, wind, wheels, collision manager).
- game_audio host: native runtime, player audio state from our physics, world emitters from the maps' .ems files,
random distant one-shots, zone ambience, state log, e2e render harness, timing.
- Setup: audio export of the banks, grains, MixMap, vault tuning and reverb zones.
- skate-core: per-wheel / per-part audio surfaces and seam patterns (additive).
- Docs: docs/hails-additions/11-audio.md and the PR plan.
Replaces our own water placement (water.rs: water collision grouped into
bodies, a random 1-2.5 s piece every 0.7-1.3 s that piled up 2-3 deep, which
the user heard as overlapping water at the University plaza).
Setup (audio manifest v4):
- audio_formats: .ems reader (72-byte records) and name_id, Bob Jenkins'
lookup8 64-bit hash with the seed the sound ids use;
- audio_export: every .ems file with each sound's attributes from the disc's
skatercollections emitter class 0xF0CEF367088EFFF8 (volume, bank, patch
selector, volume type, falloff), resolved through parents; decodes every
bank the sfx_*/skateschool emitters place (100 banks; all 486 sound
emitters resolve to a bank on the disc).
Game (game_audio/emitters.rs, behaviour taken from TU3):
- sphere or ellipsoid (semi-axes = extents along forward/up/side), inner core
at full level, level = volume x falloff ((1-d)^2, 1-d or flat);
- at most 5 emitters, in discovery order, released when the listener leaves;
- each water bank plays its program's measured pattern (two-voice relay of
shuffled one-shots or one loop, with its level/pitch movement) until the
AEMS evaluator is ported.
Verified muted in game: University plaza starts water_fountain and the
channel fountains_waterlaps_left; Aletown canal fountains_waterlaps_left.
The WAV-header byte string held literal NUL bytes, so git stored the whole
file as binary (no diffs). Written as \x00 escapes; same bytes at compile
time.
PR E merged upstream; PR A trimmed to the long-paths change (upstream fixed
the extract-xiso order itself); new row K (solver iterations); upstream sync
notes and known-failure list updated.
- Pop, flip-catch and landing cues built from the layers retail plays per
moment (bank, record, level, delay, envelope measured from TU3 traces),
with hollow/normal landing tables and per-kind chances.
- One-shots start at their final level (no fade-in eating the transient);
loops still fade in.
- Riding bed from the measured statistics.
- Master volume defaults to 75 % (100 % is unity gain; 25 % was too quiet
to judge retail parity).
The stock RWMaxIterations (25) is only the Simulation's setup value; TU3
solves with 50 on every gameplay frame. With 25 the constraints converged
less: wheels sank ~10x faster at rest, the ollie deck sat ~1 cm low and
the jump velocity the pop sound reads was too small (rolling ollie
0.21-0.29 vs retail 0.50), so pops picked the quiet row.
Maps validate identically; water traces stay within expectations. See
docs/hails-additions/12-solver-iterations.md.