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
- Add an
AudioSourcecomponent to your GameObject. - Add
NetworkAudioSourceto the same GameObject (it auto-assigns the AudioSource viaReset()). - If you plan to change clips at runtime or use
PlayOneShot, register thoseAudioClipassets in yourNetworkAssetsScriptableObject.
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 ->
ServerRpcto server -> server applies locally ->ObserversRpc(excludeSender: true)to other clients. - Late joiner ->
OnObserverAddedsendsTargetRpcwith full state to the new observer.
Playback Time
- Time is only sent when explicitly changed (via
Play(),UnPause(), or thetimesetter). - 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
nullon the remote side. - If you never change the clip at runtime, it is never serialized (the
Clipdirty flag is never set), so it doesn't matter whether it's registered.