# Network Audio Source

Drop-in networked wrapper for Unity's `AudioSource`. Add it to any GameObject with an AudioSource to sync audio across the network.

### Setup

1. Add an `AudioSource` component to your GameObject.
2. Add `NetworkAudioSource` to the same GameObject (it auto-assigns the AudioSource via `Reset()`).
3. If you plan to change clips at runtime or use `PlayOneShot`, register those `AudioClip` assets in your `NetworkAssets` ScriptableObject.

> If the clip is baked into the prefab/scene and never changes at runtime, you do **not** need to register it in NetworkAssets.

### Authority

The `_ownerAuth` toggle (Inspector) controls who can drive the audio:

* **Owner Auth (default):** The owning client controls playback. If no owner is set, the server controls it.
* **Server Auth:** Only the server can control playback.

All public setters and methods guard with `IsController(_ownerAuth)` and silently no-op on non-controllers.

### Public API

Use `NetworkAudioSource` the same way you would use Unity's `AudioSource`:

#### Properties

| Property       | Type        | Description                                                       |
| -------------- | ----------- | ----------------------------------------------------------------- |
| `clip`         | `AudioClip` | The clip to play. Must be in NetworkAssets if changed at runtime. |
| `volume`       | `float`     | Volume (0.0 - 1.0)                                                |
| `pitch`        | `float`     | Pitch multiplier                                                  |
| `loop`         | `bool`      | Whether to loop                                                   |
| `mute`         | `bool`      | Whether muted                                                     |
| `spatialBlend` | `float`     | 2D (0.0) to 3D (1.0) mix                                          |
| `minDistance`  | `float`     | Distance where volume stops increasing                            |
| `maxDistance`  | `float`     | Distance where attenuation stops                                  |
| `time`         | `float`     | Playback position in seconds                                      |
| `isPlaying`    | `bool`      | Read-only, current play state                                     |

#### Methods

| Method                          | Description                                      |
| ------------------------------- | ------------------------------------------------ |
| `Play()`                        | Start playback with the current clip             |
| `Play(AudioClip)`               | Set clip and start playback                      |
| `Stop()`                        | Stop playback                                    |
| `Pause()`                       | Pause playback                                   |
| `UnPause()`                     | Resume from pause                                |
| `PlayOneShot(AudioClip, float)` | Fire-and-forget SFX (does not affect play state) |

### How It Works

#### Dirty Flag System

Each property setter and method sets a bit in a `AudioDirtyFlags` bitmask. On the next network tick, only the flagged fields are serialized and sent. If nothing changed since the last tick, nothing is sent.

Example: calling only `volume = 0.5f` sends 2 bytes (flags) + 4 bytes (float) = **6 bytes**, not the full \~35 byte state.

#### Custom Serialization

The `AudioSourceDelta` struct implements `IPacked` for hand-written bit-level serialization. It writes the flags bitmask first, then only the fields whose bits are set. Booleans (`loop`, `mute`) are packed as single bits.

#### Channel Selection

The dirty flags are checked each tick to decide reliable vs unreliable delivery:

| What changed                            | Channel        | Reason                                                               |
| --------------------------------------- | -------------- | -------------------------------------------------------------------- |
| `Play`, `Stop`, `Pause`, `UnPause`      | **Reliable**   | Discrete state transition; a dropped packet means permanent desync   |
| `clip`                                  | **Reliable**   | Discrete change; must arrive                                         |
| `volume`, `pitch`, `spatialBlend`, etc. | **Unreliable** | Continuous tweaks (fades); next tick resends latest value if dropped |
| `PlayOneShot`                           | **Reliable**   | Fire-and-forget SFX; must not be missed                              |
| Late joiner reconcile                   | **Reliable**   | Full state snapshot; must arrive                                     |

If a tick contains both reliable and unreliable changes (e.g. `volume = 0.5f` + `Play()` in the same frame), the entire delta is sent reliably.

#### RPC Routing

Follows the same pattern as `NetworkAnimator`:

* **Server controller** -> `ObserversRpc(excludeSender: true)` directly to clients.
* **Client controller** -> `ServerRpc` to server -> server applies locally -> `ObserversRpc(excludeSender: true)` to other clients.
* **Late joiner** -> `OnObserverAdded` sends `TargetRpc` with full state to the new observer.

#### Playback Time

* Time is only sent when explicitly changed (via `Play()`, `UnPause()`, or the `time` setter).
* It is **not** auto-sent every tick while playing, avoiding constant bandwidth for background music.
* On receiving end, if a time value arrives while playing and the local playback has drifted more than 0.1s, it snaps to the received time.

#### AudioClip Serialization

AudioClips are serialized via PurrNet's `Packer<T>` for `UnityEngine.Object`, which looks up the clip's index in `NetworkAssets`. This sends a small integer index, not the audio data.

* If the clip **is not** in NetworkAssets, it resolves to `null` on the remote side.
* If you never change the clip at runtime, it is never serialized (the `Clip` dirty flag is never set), so it doesn't matter whether it's registered.
