makepad/skills/makepad-audio-generator/references/audio-workflows.md
Arena Agent fe21c07d84 skills: add Makepad game skills pack ported from threejs-game-skills
Nine agent skills for building Makepad/Rust games, ported from
majidmanzarpour/threejs-game-skills. Same director-routed workflow and
premium bar; runtime rewritten for this fork's game crates.

- makepad-game-director        entrypoint, routing, continuity, asset probe
- makepad-gameplay-systems     loop, movers vs rigid bodies, input, camera, netplay
- makepad-aaa-graphics-builder lighting, shaders, budget, visual scorecard
- makepad-game-ui-designer     HUD, menus, touch and XR UI
- makepad-debug-profiler       defect bisection and profiling
- makepad-qa-release           verification ladder, evidence, packaging
- makepad-3d-generator         CC0 model search, casts, procedural geometry
- makepad-image-generator      texgen textures, palettes, sky, icons
- makepad-audio-generator      sample bank, mixer, material impacts, 3D audio

Written against the real APIs in libs/game/*, libs/sim and apps/arcade:
the game.* verb table, GameRenderer adaptive quality, the packed 6-float
GameMeshVertex layout, script_mod! splash styling, makepad-test driving,
and the BUDGETS.md numbers. No paid generation API is required - the CC0
library plus seeded makepad-game-gen replaces them.

Includes install.sh (Codex/Claude), validate-skills.sh and a repo-aware
probe_assets.sh; both scripts verified against this checkout.
2026-09-05 21:18:58 +00:00

4.4 KiB
Raw Permalink Blame History

Audio workflows

Loading

let pcm = makepad_game_audio::decode(&bytes)?;   // WAV or Ogg Vorbis -> Pcm
let id: SampleId = bank.add(pcm);

SampleBank owns loaded samples; SampleId is the handle you keep. Load at startup or during level load — decoding during play is a stutter source, and Vorbis decode is the most expensive audio operation in the stack.

AudioError distinguishes decode failures. Handle it: a missing or corrupt sound should log and continue, never panic mid-frame.

Playback

let handle: VoiceHandle = mixer.play(VoiceSpec {
    sample: id,
    gain,
    pitch,
    priority: Priority::/* .. */,
    // placement for positional voices
});

Keep VoiceHandles for anything you must stop or modulate later — looping engine notes, ambience beds. Fire-and-forget for one-shots.

Event-driven emission

for impact in dynamics.take_mover_impacts() {
    let pair = MaterialPair::new(impact.a_material, impact.b_material);
    director.emit(SoundEvent {
        category: Category::Impact,
        placement: Placement::At(impact.pos),
        // gain shaped by ImpactCurve from impact energy
    });
}

take_mover_impacts() is the single source of truth for collisions. Re-detecting collisions in gameplay code to trigger audio is how you get double sounds that drift out of sync with the visual effect.

Material table

Material, MaterialPair, ImpactCurve.

Map every pair a scene can produce; an unmapped pair either falls back to a generic thud (annotated-sounding) or is silent (feels broken). Enumerate the materials in the scene and fill the matrix — it is small, and it is the difference between a world that sounds physical and one that sounds labelled.

ImpactCurve maps impact energy to gain and brightness: a light tap and a heavy crash should not be the same sample at the same level. Energy comes from the impact, not from a fixed constant per event type.

Variation

let pitch = 1.0 + rng.range(-0.08, 0.08);
let gain  = base * (1.0 + rng.range(-0.10, 0.10));
let sample = variants[rng.next_index(variants.len())];

LocalRng, seeded — never thread RNG, or networked clients hear different things and any replay diverges.

For sample choice, shuffle a bag rather than picking uniformly at random: pure random repeats the same variant back-to-back often enough to be audible, which defeats the purpose.

Voice budget

Category Suggested cap
Player feedback always plays, never evicted
UI 4
Nearby impacts 8
Footsteps 6, coalesced
Ambience 24 loops
Music 1

Enforce with Priority. Cull by distance before mixing. Coalesce identical events within a frame into one louder voice. Debounce repeats of the same sample within ~30 ms — overlapping identical copies phase-cancel into a metallic artefact that sounds like a bug, because it is one.

3D

let listener = Listener { pos: camera.pos, forward: camera.forward, up: camera.up };

On the camera, not the player. Attenuate roughly inverse-square with a near clamp, cull beyond a max range, pan by the listener's right vector. Keep UI and player-feedback sounds non-positional so they never get quiet or pan away at the moment they matter.

Ambience

One or two long loops, quiet, continuous, crossfaded on area change. Layer sparse one-shots (a bird, a distant creak) at randomised intervals — this is what stops a loop being recognisable as a loop, and it is much cheaper than a longer bed.

Music and ducking

Music sits under everything. Duck it (and ambience) by a few dB with a fast attack and slow release when player feedback fires; this is what keeps important sounds audible without raising their level to the point of harshness.

The Kenney jingle pack covers win/lose/level-complete stings. Short stings beat looping tracks for most game-jam-scope games — cheaper, and they never outstay their welcome.

Testing

cargo test -p makepad-game-audio

Cx-free, so it all tests headlessly. Assert: decode round-trips (rate, channels, length); the mixer honours its voice cap under a flood of events; priority evicts the lowest-priority voice, never the player feedback; every MaterialPair a scene can produce resolves; no emission path grows a Vec without bound.

Then listen for several minutes of real play. Numbers cannot tell you that a footstep is repeating audibly, that the mix collapses in a busy wave, or that a critical event has no sound at all.