# Audio

The mixer lives in `crates/audio` (`src/mixer.rs`); its module docs are the full reference for
buses, snapshots, music and the voice cap. This page covers the policy decisions a game author
needs to know about without reading the code.

## Several listeners (local split screen)

Local co-op has two to four views on one screen and one stereo output device. Each view has a
camera, so each has a listener, and the mixer has to decide what one spatial sound is like when
the listeners are in different places.

**Default: the nearest listener decides.** For each spatial voice the listener closest to it
sets both its distance attenuation and its pan, exactly as if that player were alone. If two
listeners are exactly as close, the one earlier in the list wins, so pass the views in a stable
order with player 1 first. A sound next to player 2 sounds the way it would in a one-player
game, and a sound far from every player stays quiet.

**The alternative: `Sum`.** `ListenerPolicy::Sum` adds up each listener's left and right
contribution, then scales the total down so the voice is never louder than its unattenuated
gain. Two players standing on the same sound hear it no louder than one would. The cost is that
a sound between two players comes out of both speakers at once, so neither player can tell
which side of their own screen it is on. Pick it for a shared-space game where loudness should
reflect how many players are near a sound. Set it with `Mixer::set_listener_policy`.

**Considered and rejected:**

- *The loudest listener decides.* This is the same as the nearest one. Every rolloff curve
  falls as distance grows, and every listener hears a voice through that voice's own curve and
  range.
- *Centred pan when listeners disagree.* This throws away the one cue that tells a player
  which side of their own view a sound is on.

**Guarantees.**

- One listener gives bit-for-bit the same output as the single-listener mixer under either
  policy. `crates/audio/tests/listeners.rs` checks this every tick, bit for bit, against the old
  single-listener formula.
- Which listener is chosen never affects voice priority, voice stealing or the handle `play`
  returns, so the simulation stays deterministic.
- Listeners are held in a fixed array of `MAX_LISTENERS` (4). Setting them every tick
  allocates nothing (`crates/audio/tests/alloc.rs`).
- Bad input is contained (`crates/audio/tests/listener_adversarial.rs`). A listener whose
  position is not finite never wins the nearest decision, so it cannot silence a real one; a
  listener with a NaN or zero rotation pans to the centre. Whatever goes in, gain, pan and both
  ears are finite and no louder than the voice's own gain.
- Listeners are cut to the first four in the order given, so the cut is stable.
- When the nearest listener changes, gain stays continuous (it follows the nearest distance) but
  pan steps to the new listener's side. Smoothing that step belongs to the backend.
- The mixer has no doppler, occlusion or per-listener reverb today. If any is added, it will
  use the listener the policy chose.

**API.**

- Mixer: `Mixer::set_listeners(&[Listener])` takes 1 to 4 listeners. Extra listeners are
  ignored, and an empty slice changes nothing. `set_listener` is the one-listener case.
- Mixer queries: `Mixer::listeners()` returns the live listeners, and `VoiceGain::listener`
  is the index of the listener that decided that voice.
- App: `World::set_listener_sources(&[(Vec3, Quat)])` takes one position and rotation per
  view and applies them from the next tick. An empty slice goes back to the default below.
- **Views listen by themselves.** With a `ctx.views` layout of two or more views
  (`left-right`, `top-bottom`, `three`, `grid`) the mixer gets one listener per view, view 1
  first, at each view's camera, refreshed every tick as the cameras move; a view the layout
  drops stops listening on that tick. View 1's listener is the camera's, exactly where a
  one-view game's is. With one view nothing changes: one listener on the camera, bit for bit.
  A scene's `output: "viewport"` camera (a minimap) is not a view and does not listen. Nothing
  is allocated per tick. `crates/app/tests/split_screen_audio.rs` covers this.
- Script: `ctx.audio.listenFrom("views" | "camera")`. `"views"` is the default above;
  `"camera"` keeps one listener on view 1's camera however many views there are.
  `ctx.audio.setListenerPolicy("nearest" | "sum")` is `Mixer::set_listener_policy`. Both
  apply from the next tick, may be called in `init` before any view exists, and throw a
  `TypeError` for any other value, changing nothing.
- Who listens, first that applies: `World::set_listener_sources` (non-empty); then
  `listenFrom("camera")`; then one listener per view; then the camera.

