# 3. Character Audio Effects Chain
Part of [[Audio Mixers and Character Audio]]. Previous: [[2. Audio Mixer Snapshots and Exposed Parameters]]. Next: [[4. Footsteps and Surface Audio]].
> [!note] Verification status
> `AudioLowPassFilter.cutoffFrequency` / `lowpassResonanceQ`, `AudioReverbFilter`, `AudioSource.spatialBlend`, `pitch`, `priority`, `loop` checked in the 6000.5 reference (2026-11-07). Uses `SnapshotDirector` and `MixerGroups` from Parts 1-2. **Code not compiled in Unity.**
## 3.1 Two places to put an effect
| | Mixer group effect | Filter component on the GameObject (`AudioLowPassFilter`, `AudioHighPassFilter`, `AudioEchoFilter`, `AudioDistortionFilter`, `AudioReverbFilter`, `AudioChorusFilter`) |
|---|---|---|
| Applies to | every source routed to the group, *after* summing | that one source (or listener), before the mixer |
| Cost | one instance per group | one instance per source |
| Snapshot-able | **yes** | no (script it) |
| Best for | category moods (underwater, pause, low health) | per-emitter state: occlusion, a robot enemy's distortion, a radio voice |
Rule: if *all player sounds* should change, use the `SFX/Player` group. If *this one enemy behind a wall* should change, use a filter on its source.
## 3.2 The player audio rig
Under the player prefab (works with either controller from [[Character Controllers]]):
```
Player
|- Audio_Feet AudioSource -> SFX/Player/Footsteps (at feet)
|- Audio_Voice AudioSource -> SFX/Player/Voice (at head)
|- Audio_Foley AudioSource -> SFX/Player/Foley (at chest: cloth, gear rattle)
\- Audio_Heart AudioSource -> SFX/Player/Heartbeat (loop, 2D)
```
Spatial blend guide:
- **First person:** player sources 2D (`spatialBlend = 0`) - the listener is in your head.
- **Third person:** ~0.5-0.8 so steps sit in the world but stay readable when the camera orbits away.
- **VR (Quest):** your own footsteps are usually omitted or very subtle - real feet don't match virtual ones; spatial-audio specifics are a later tutorial in the [[Audio]] series.
Priority: player voice/feet `priority` 64; world props 128+ (0 = highest, 256 = lowest). Per the 6000.5 reference, when active sources exceed the project's max **real voices**, Unity *virtualizes* the lowest-priority sources first (muted but still tracking position, so they can resume), and among equal priorities the quieter one.
## 3.3 Character audio state component
One component listens to gameplay state and drives the mixer - gameplay code never touches the mixer directly (Observer + a single owner of mix state).
```csharp
using UnityEngine;
using UnityEngine.Audio;
/// Drives player-related mix state: low-health blend + heartbeat, underwater, power-up pitch.
public class PlayerAudioState : MonoBehaviour
{
[Header("Refs")]
[SerializeField] SnapshotDirector snapshots;
[SerializeField] AudioMixer mixer;
[SerializeField] AudioSource heartbeat; // looping clip on SFX/Player/Heartbeat
[Header("Heartbeat")]
[SerializeField] float heartbeatMinPitch = 0.9f;
[SerializeField] float heartbeatMaxPitch = 1.5f;
[Header("Power-up voice")]
[Tooltip("Exposed Pitch parameter on SFX/Player/Voice (e.g. a Pitch Shifter's Pitch).")]
[SerializeField] string voicePitchParam = "PlayerVoicePitch";
float _health01 = 1f;
bool _underwater;
// ----- inputs from gameplay (call these from your health/stat system) -----
public void OnHealthChanged(float current, float max)
{
_health01 = max > 0f ? Mathf.Clamp01(current / max) : 0f;
if (!_underwater) snapshots.BlendHealth(_health01);
UpdateHeartbeat();
}
public void SetUnderwater(bool value)
{
if (_underwater == value) return;
_underwater = value;
if (_underwater) snapshots.ToUnderwater();
else snapshots.BlendHealth(_health01); // return to health-appropriate mix
}
public void SetPoweredUp(bool on, float pitch = 1.25f)
{
if (on) mixer.SetFloat(voicePitchParam, pitch);
else mixer.ClearFloat(voicePitchParam); // hand control back to snapshots
}
void UpdateHeartbeat()
{
float danger = Mathf.InverseLerp(0.4f, 0.1f, _health01);
if (danger > 0f && !heartbeat.isPlaying) heartbeat.Play();
if (danger <= 0f && heartbeat.isPlaying) heartbeat.Stop();
heartbeat.pitch = Mathf.Lerp(heartbeatMinPitch, heartbeatMaxPitch, danger);
// Level comes from the LowHealth snapshot (Heartbeat group fader), so no volume code here.
}
}
```
> [!warning] Code not compiled in Unity
> Not compiled. Hook `OnHealthChanged` to your health stat - e.g. the derived stats in [[1.Stat Definitions and Base vs Derived Stats]] or a timed buff from [[2.Modifiers and Timed Buffs]] for `SetPoweredUp`. The pitch shifter's exposed parameter range/units depend on the effect - check the Pitch Shifter page in the effects reference.
## 3.4 Underwater volume
```csharp
using UnityEngine;
/// Put on a trigger volume covering the water. Tracks the camera/head, not the body,
/// so the mix changes when the *ears* go under.
[RequireComponent(typeof(Collider))]
public class UnderwaterAudioVolume : MonoBehaviour
{
[SerializeField] PlayerAudioState playerAudio;
[SerializeField] Transform listenerTransform; // usually the camera with the AudioListener
Collider _volume;
void Awake()
{
_volume = GetComponent<Collider>();
_volume.isTrigger = true;
}
void Update()
{
// Polling the listener point is more robust than trigger callbacks on a camera with no Rigidbody.
bool inside = _volume.bounds.Contains(listenerTransform.position)
&& (_volume.ClosestPoint(listenerTransform.position) - listenerTransform.position).sqrMagnitude < 0.0001f;
playerAudio.SetUnderwater(inside);
}
}
```
> [!warning] Code not compiled in Unity
> Not compiled. `Collider.ClosestPoint` returns the point itself when the point is inside a convex collider - that's the inside test; for a concave MeshCollider use primitive colliders instead. With several water volumes, aggregate them (count) before calling `SetUnderwater`.
## 3.5 Per-source occlusion on an enemy voice
A character effect that **must** be per-source: muffle an enemy (or NPC) when a wall is between them and the listener. Directly supports the sensing game in [[Organic Enemy Sensing]] - the player hears the guard muffled through a wall *before* seeing it.
```csharp
using UnityEngine;
[RequireComponent(typeof(AudioSource), typeof(AudioLowPassFilter))]
public class SimpleOcclusion : MonoBehaviour
{
[SerializeField] Transform listener;
[SerializeField] LayerMask occluders; // walls, not props
[SerializeField] float openCutoff = 22000f;
[SerializeField] float occludedCutoff = 1200f;
[SerializeField] float occludedVolume = 0.6f;
[SerializeField] float smoothing = 8f;
[SerializeField] float checkInterval = 0.1f;
AudioSource _src;
AudioLowPassFilter _lp;
float _baseVolume, _targetCutoff, _targetVol, _nextCheck;
void Awake()
{
_src = GetComponent<AudioSource>();
_lp = GetComponent<AudioLowPassFilter>();
_baseVolume = _src.volume;
_targetCutoff = openCutoff;
_targetVol = _baseVolume;
if (listener == null && Camera.main) listener = Camera.main.transform;
}
void Update()
{
if (Time.time >= _nextCheck && _src.isPlaying)
{
_nextCheck = Time.time + checkInterval;
bool blocked = Physics.Linecast(transform.position, listener.position, occluders, QueryTriggerInteraction.Ignore);
_targetCutoff = blocked ? occludedCutoff : openCutoff;
_targetVol = blocked ? _baseVolume * occludedVolume : _baseVolume;
}
float k = 1f - Mathf.Exp(-smoothing * Time.deltaTime);
_lp.cutoffFrequency = Mathf.Lerp(_lp.cutoffFrequency, _targetCutoff, k);
_src.volume = Mathf.Lerp(_src.volume, _targetVol, k);
}
}
```
> [!warning] Code not compiled in Unity
> Not compiled. Linear-interpolating Hz sounds uneven; for a nicer sweep lerp in log space (`Mathf.Exp(Mathf.Lerp(Mathf.Log(a), Mathf.Log(b), k))`). For many emitters, move the linecasts to a manager that staggers checks (budget idea from [[5.Quest 2 Performance and Rendering]]).
## 3.6 Teaching exercise (Level 16 + Level 8)
Remove the health bar from the HUD. Can players still tell they're about to die from audio alone (heartbeat + muffled mix)? Then put the bar back and ask which version felt more tense. Clean screen vs information - Rogers' Level 8 trade-off, solved with sound.
## Docs
- Audio effects (mixer): https://docs.unity.com/en-us/engine/6000.5/manual/audio/reference/effects
- Audio filters (components): https://docs.unity.com/en-us/engine/6000.5/manual/audio/reference/filters
- `AudioLowPassFilter`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/AudioLowPassFilter.html
- `AudioReverbFilter`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/AudioReverbFilter.html
- `AudioSource.spatialBlend`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/AudioSource-spatialBlend.html
- `AudioSource.priority`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/AudioSource-priority.html
- `AudioMixer.ClearFloat`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Audio.AudioMixer.ClearFloat.html