# Audio workflows ## Loading ```rust 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 ```rust let handle: VoiceHandle = mixer.play(VoiceSpec { sample: id, gain, pitch, priority: Priority::/* .. */, // placement for positional voices }); ``` Keep `VoiceHandle`s for anything you must stop or modulate later — looping engine notes, ambience beds. Fire-and-forget for one-shots. ## Event-driven emission ```rust 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 ```rust 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 | 2–4 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 ```rust 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 ```bash 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.