# AudioHandle

> Talos next · Since 0.6; changed in 0.7

Mixer control: the bus graph, its snapshots, the music layers, and one-shot cues.

Reached in a behaviour as `ctx.audio`.

## Signature

```ts
export interface AudioHandle {
  /** Start a sound from the scene's assets; returns a handle for `stop`. */
  play(asset: string, opts?: AudioOptions): number
  stop(handle: number): void
  /**
   * Change the playback rate of a sound that is playing, e.g. an engine loop following RPM.
   * `rate` 1 is as authored; it must be finite and above 0. With `rampSeconds` the rate glides
   * there linearly in sim time (no zipper noise) from wherever it is now; without, it steps.
   * A new call replaces a ramp in flight. A handle that has ended, was never valid, or was
   * refused by `play` (0) does nothing and does not throw, as with {@link stop}.
   * Calls apply in call order at the next sim tick.
   *
   * @since 0.6.0
   */
  setPitch(handle: number, rate: number, opts?: AudioRampOptions): void
  /**
   * Change the volume (0..1, as {@link AudioOptions.volume}) of a sound that is playing, with
   * the same ramp and no-op rules as {@link setPitch}. This is the voice's own volume; the bus
   * it plays on still applies on top.
   *
   * @since 0.6.0
   */
  setVolume(handle: number, volume: number, opts?: AudioRampOptions): void
  /** A handle onto one bus of `meta.audio.buses`. */
  bus(name: AudioBus): AudioBusHandle
  /**
   * Cross-fade every bus to a named snapshot over that snapshot's own transition time.
   * Buses the snapshot does not name go back to their authored volume, so snapshots
   * replace each other rather than stacking. False when there is no such snapshot.
   */
  snapshot(name: string): boolean
  /**
   * Where the listeners are, from the next tick on. `"views"` (the default): with a split-screen
   * layout of two or more views ({@link ViewsHandle.setLayout}) each view has a listener at its own
   * camera, view 1 first, so a sound next to player 2 is heard as player 2 would hear it; with one
   * view the one listener is the camera, as always. `"camera"`: one listener at view 1's camera
   * however many views there are. Any other mode throws a `TypeError` and changes nothing.
   *
   * @since 0.6.1
   */
  listenFrom(mode: AudioListenFrom): void
  /**
   * How several listeners hear one spatial sound (docs/AUDIO.md "Several listeners"): `"nearest"`
   * (the default) lets the nearest listener set its volume and pan; `"sum"` adds up every listener's
   * share, never louder than the sound itself. With one listener both give the same output. Any
   * other policy throws a `TypeError` and changes nothing.
   *
   * @since 0.6.1
   */
  setListenerPolicy(policy: AudioListenerPolicy): void
  readonly music: AudioMusicHandle
}
```

Import: `import type { AudioHandle } from "@argus/api"`

## Members

| Member | Signature | Description |
|---|---|---|
| `play` | `play(asset: string, opts?: AudioOptions): number` | Start a sound from the scene's assets; returns a handle for `stop`. |
| `stop` | `stop(handle: number): void` |  |
| `setPitch` | `setPitch(handle: number, rate: number, opts?: AudioRampOptions): void` | Change the playback rate of a sound that is playing, e.g. an engine loop following RPM. `rate` 1 is as authored; it must be finite and above 0. With `rampSeconds` the rate glides there linearly in sim time (no zipper noise) from wherever it is now; without, it steps. A new call replaces a ramp in flight. A handle that has ended, was never valid, or was refused by `play` (0) does nothing and does not throw, as with {@link stop}. Calls apply in call order at the next sim tick. @since 0.6.0 |
| `setVolume` | `setVolume(handle: number, volume: number, opts?: AudioRampOptions): void` | Change the volume (0..1, as {@link AudioOptions.volume}) of a sound that is playing, with the same ramp and no-op rules as {@link setPitch}. This is the voice's own volume; the bus it plays on still applies on top. @since 0.6.0 |
| `bus` | `bus(name: AudioBus): AudioBusHandle` | A handle onto one bus of `meta.audio.buses`. |
| `snapshot` | `snapshot(name: string): boolean` | Cross-fade every bus to a named snapshot over that snapshot's own transition time. Buses the snapshot does not name go back to their authored volume, so snapshots replace each other rather than stacking. False when there is no such snapshot. |
| `listenFrom` | `listenFrom(mode: AudioListenFrom): void` | Where the listeners are, from the next tick on. `"views"` (the default): with a split-screen layout of two or more views ({@link ViewsHandle.setLayout}) each view has a listener at its own camera, view 1 first, so a sound next to player 2 is heard as player 2 would hear it; with one view the one listener is the camera, as always. `"camera"`: one listener at view 1's camera however many views there are. Any other mode throws a `TypeError` and changes nothing. @since 0.6.1 |
| `setListenerPolicy` | `setListenerPolicy(policy: AudioListenerPolicy): void` | How several listeners hear one spatial sound (docs/AUDIO.md "Several listeners"): `"nearest"` (the default) lets the nearest listener set its volume and pan; `"sum"` adds up every listener's share, never louder than the sound itself. With one listener both give the same output. Any other policy throws a `TypeError` and changes nothing. @since 0.6.1 |
| `music` | `readonly music: AudioMusicHandle` |  |

## Example

The tested fixture `fixtures/recipes/jump-pad-and-elevator/scenes/behaviors/camera-zone.ts`.

```ts
import type { Behavior } from "@argus/api"

/**
 * A trigger volume that retunes the camera while the player is inside it.
 *
 * The engine has no camera-zone concept and needs none: a volume, a `trigger` component and this.
 * Here the player's behaviour owns the camera (it re-sends its params when the view is turned), so
 * the zone tells it what to change rather than calling `ctx.camera.set` itself. In a scene where
 * nothing else writes the camera, the zone can call `ctx.camera.set(kind, params)` directly.
 */
interface Props {
  /** `ctx.camera.set` params to merge in on the way in, and on the way out. */
  inside?: Record<string, unknown>
  outside?: Record<string, unknown>
}

export default {
  onTrigger(ctx, trigger) {
    const params = trigger.entering ? ctx.props.inside : ctx.props.outside
    if (params) ctx.events.emit("camera-zone", { target: trigger.other.id, params })
  },
} satisfies Behavior<Record<string, never>, Props>
```
