> For the complete documentation index, see [llms.txt](https://docs.allout.game/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.allout.game/core-engine-concepts/sound-effects.md).

# Sound and Haptics

Play sound effects and local impact feedback.

Use the SFX API to play short sound effects (UI clicks, footsteps, impacts) and simple looping audio (ambient loops, music).

{% hint style="warning" %}
Currently only **PCM-encoded `.wav`** files are supported. If you have a `.wav` that won’t import/play, convert it to PCM using Audacity (or a similar tool).
{% endhint %}

### Quick start (play a sound)

```go
click := get_asset(SFX_Asset, "sfx/click.wav");

desc := SFX.default_sfx_desc();
SFX.play(click, desc);
```

### SFX API reference

```go
SFX_Asset :: class : Asset {}

SFX_Channel :: enum {
    SFX;
    MUSIC;
}

SFX_Desc :: struct {
    specific_to_player: Player;
    positional:       bool;
    position:         v2 #read_only;
    delay:            float;
    volume:           float;
    speed:            float;
    volume_perturb:   float;
    speed_perturb:    float;
    range_multiplier: float;
    loop_timeout:     float;
    entity_to_follow: u64;
    loop:             bool;
    channel:          SFX_Channel;
    unique_id:        u64;

    set_position :: method(p: v2);
}

SFX :: struct {
    play              :: proc(asset: SFX_Asset, desc: SFX_Desc) -> u64;
    stop              :: proc(id: u64);
    fade_out_and_stop :: proc(id: u64, fade_time: float);
    default_sfx_desc :: proc() -> SFX_Desc;
}
```

### Getting an `SFX_Asset`

Sound effects live in your game’s `res` folder (often under `res/sfx/`).

* Drag sounds in from the **Asset Catalog** (editor downloads them into `res/`)
* Or add your own `.wav` file into `res/` from your local machine

Then reference them by path:

```go
pickup := get_asset(SFX_Asset, "sfx/pickup.wav");
```

{% hint style="warning" %}
Check asset paths during development. A missing asset produces a null handle; `SFX.play` logs a warning and returns `0`.
{% endhint %}

{% hint style="info" %}
Some built-in platform assets use `$AO/...` paths. For example: `get_asset(SFX_Asset, "$AO/sfx/FUI Hologram Ping Tone Echoed.wav")`.
{% endhint %}

### Positional (3D-ish) sounds

By default, SFX plays as a non-positional “2D” sound. To make it positional, set a position:

```go
play_hit_sfx :: proc(world_pos: v2) {
    hit := get_asset(SFX_Asset, "sfx/hit.wav");

    desc := SFX.default_sfx_desc();
    desc.set_position(world_pos);
    desc.range_multiplier = 1.5;

    SFX.play(hit, desc);
}
```

### Following an entity (moving sound source)

If a sound should move with an entity (engine hum, buzzing projectile, etc), set `entity_to_follow`:

```go
start_engine_loop :: proc(entity: Entity) -> u64 {
    engine := get_asset(SFX_Asset, "sfx/engine_loop.wav");

    desc := SFX.default_sfx_desc();
    desc.entity_to_follow = entity.id;
    desc.loop = true;
    desc.channel = .MUSIC; // optional: treat as music/ambient instead of SFX

    // Returns an ID that can be used to update or stop the sound.
    return SFX.play(engine, desc);
}
```

{% hint style="info" %}
If `entity_to_follow` is set, you usually don’t need to also call `set_position` (it’s fine if you do).
{% endhint %}

### Player-targeted sounds

Set `specific_to_player` when the server should track the sound but only one client should actually hear it:

```go
play_private_ping :: proc(player: Player) {
    click := get_asset(SFX_Asset, "sfx/click.wav");

    desc := SFX.default_sfx_desc();
    desc.specific_to_player = player;
    desc.volume = 0.7;

    SFX.play(click, desc);
}
```

### Looping + stopping

`SFX.play` returns an ID you can stop later:

```go
sound_id := 0.(u64);

start_loop :: proc() {
    loop := get_asset(SFX_Asset, "sfx/ambience.wav");

    desc := SFX.default_sfx_desc();
    desc.loop = true;
    desc.volume = 0.6;
    desc.loop_timeout = 60; // safety: auto-stop after ~60s if you forget

    sound_id = SFX.play(loop, desc);
}

stop_loop :: proc() {
    if sound_id != 0 {
        SFX.stop(sound_id);
        sound_id = 0;
    }
}
```

### Rapid repeated sounds and variation

For quick repetitive sounds such as footsteps or automatic weapons, give each logical event a stable `unique_id`. Scope a synchronized monotonic event index to its source entity when the sound does not follow that entity:

```go
footstep_id += 1; // a synchronized u64 field in gameplay state

desc := SFX.default_sfx_desc();
desc.set_position(entity.world_position);
desc.unique_id = mix_u64(entity.id, footstep_id);
SFX.play(get_asset(SFX_Asset, "$AO/sfx/Footsteps/footstep_01.wav"), desc);
```

The same logical event must retain the same `unique_id` on the client, server, and every resimulation. Every distinct nearby event must get a different value. Only derive it from an event index that is synchronized and stable through prediction. `entity_to_follow` participates in sound identity, so when it is assigned, use the event index directly instead of mixing the entity ID into `unique_id`.

Subtle variation can keep frequently played sounds from becoming monotonous:

```go
desc := SFX.default_sfx_desc();
desc.volume_perturb = 0.2;
desc.speed_perturb  = 0.15;
SFX.play(get_asset(SFX_Asset, "sfx/footstep.wav"), desc);
```

{% hint style="info" %}
Keep `volume_perturb` / `speed_perturb` subtle. Values above \~`0.3` usually sound wrong.
{% endhint %}

### Prediction and targeting

Call gameplay sounds from the same shared predicted path as the event that caused them. The sound system reconciles a client's predicted sound with the server result, so do not wrap `SFX.play` in `Game.is_server()` or `player.is_local()`.

```go
play_round_victory :: proc(position: v2) {
    desc := SFX.default_sfx_desc();
    desc.set_position(position);
    SFX.play(get_asset(SFX_Asset, "sfx/round_victory.wav"), desc);
}
```

For a sound that only one player should hear, set `desc.specific_to_player`. This keeps the event in shared gameplay code while targeting playback to that player's client.

## Haptics

`Haptics.play_impact` triggers impact feedback on the local device. Call it only for the local player:

```go
My_Player :: class : Player_Base {
    play_local_impact :: method() {
        if !is_local() return;

        Haptics.play_impact(.MEDIUM);
    }
}
```

Choose `.LIGHT`, `.MEDIUM`, or `.HEAVY`. Haptics currently work on iOS. Calls on Android, web, Windows, and servers do nothing.

Haptics are not synchronized, targeted, or deduplicated. Trigger them at the local interaction point rather than from scene-wide gameplay code.
