AppleyAppley Docs
Docs/Audio

Audio Systems

Build centralized audio managers with channels, pooling, and 3D sound.

36 min read Intermediate Updated 2026-07-01 v2.0 ~131 min to complete

Introduction

Audio systems manage all game sound. This guide covers audio managers, channel grouping, source pooling, 3D spatial audio, and dynamic music transitions.

Guide 25 of 25

This is part of the Intermediate Guides collection. Category: Audio. Estimated completion: 131 minutes. Difficulty: Intermediate.

Screenshot

Audio Systems
Audio Systems

Step-by-Step Guide

1

Create an audio manager

Build a centralized AudioManager that handles all playback. Scripts call audioManager.play() instead of audio.play() directly.

2

Set up audio channels

Group sounds into channels: Master, Music, SFX, Voice, Ambient. Each has independent volume control.

3

Pool audio sources

Pre-create 15-20 audio sources. When playing, find an idle source and assign the clip. Prevents creating/destroying per sound.

4

Configure 3D audio

For positional sounds (footsteps, gunfire), enable 3D spatial mode. Set min distance and max distance for falloff.

5

Add dynamic music

Transition between tracks based on game state (explore, combat, boss). Crossfade over 2-3 seconds.

6

Implement ducking

When voice audio plays, duck music volume by 30% for 2 seconds. Ensures dialogue is audible over music.

FluxScript Example

Here is a complete FluxScript implementation for this guide:

Audio Systems — FluxScript
const MAX_SOURCES = 20

on start {
  sources = []
  for i = 0 to MAX_SOURCES {
    src = audio.createSource()
    sources[i] = { source: src, inUse: false, channel: null }
  }
  channels = { master: 1.0, music: 0.7, sfx: 0.9, voice: 0.85, ambient: 0.5 }
  currentMusic = null
  musicState = "explore"
}

func play(soundId, channel, options) {
  options = options || {}
  src = null
  for i = 0 to MAX_SOURCES {
    if !sources[i].inUse { src = sources[i]; break }
  }
  if src == null return
  src.inUse = true
  src.channel = channel
  src.source.setClip(soundId)
  vol = channels[channel] * (options.volume || 1.0)
  src.source.setVolume(vol)
  if options.position != null {
    src.source.set3D(true)
    src.source.setPosition(options.position)
    src.source.setMinDistance(options.minDist || 3)
    src.source.setMaxDistance(options.maxDist || 30)
  }
  pitch = options.pitch || 1.0
  if options.randomPitch pitch = 0.9 + math.random(0.2)
  src.source.setPitch(pitch)
  src.source.setLoop(options.loop || false)
  src.source.play()
  if !options.loop {
    duration = audio.getClipDuration(soundId)
    timer.start("release_" + i, duration, 1)
  }
  return src
}

func playMusic(soundId, crossfadeTime) {
  crossfadeTime = crossfadeTime || 2.0
  if currentMusic != null fadeOut(currentMusic, crossfadeTime)
  currentMusic = play(soundId, "music", { loop: true, volume: 0 })
  fadeIn(currentMusic, channels.music, crossfadeTime)
}

func setMusicState(state) {
  if musicState == state return
  musicState = state
  tracks = { explore: "music/explore", combat: "music/combat", boss: "music/boss" }
  playMusic(tracks[state], 2.0)
}

func setChannelVolume(channel, volume) {
  channels[channel] = volume
  for i = 0 to MAX_SOURCES {
    if sources[i].inUse && sources[i].channel == channel {
      sources[i].source.setVolume(volume)
    }
  }
}

on event("combatStart") { setMusicState("combat") }
on event("combatEnd") { setMusicState("explore") }
on event("bossStart") { setMusicState("boss") }

Node Graph Example

Prefer visual programming? Here is the equivalent Node Graph:

Audio Systems — Node Graph
[On Start] --> [Create: 20 audio sources (pool)]
   --> [Init: channel volumes]

[Function: play] --> [Find: idle source from pool]
   --> [Set: clip, volume (channel x sound), 3D position]
   --> [Set: pitch (with random option), loop]
   --> [Play] --> [Timer: auto-release when done]

[Function: playMusic] --> [Fade Out: current music]
   --> [Play: new music, loop, volume=0]
   --> [Fade In: to music channel volume]

[Function: setMusicState] --> [Branch: state changed]
   --> [playMusic: corresponding track, 2s crossfade]

[Function: setChannelVolume] --> [Update: all sources on channel]

[On Event "combatStart"] --> [setMusicState: "combat"]
[On Event "combatEnd"] --> [setMusicState: "explore"]

Optimization Tips

Keep your implementation fast and efficient with these optimization tips:

  • Pool audio sources. Creating and destroying sources per sound causes GC spikes. Pre-create 15-20 sources and recycle.
  • Use channels for volume control. Group sounds into Music, SFX, Voice, Ambient. Lets players adjust independently in settings.
  • Stream long music tracks. Do not load entire music files into memory — stream them. Saves 50-100MB per track.
  • Use pitch randomization for repeated SFX. Footsteps and gunshots at identical pitch sound monotonous. Randomize by 10%.

Common Mistakes

Creating audio sources per sound

Pool sources. Creating/destroying per footstep causes GC spikes and limits concurrent sounds.

Music not ducking during dialogue

Implement ducking. When voice plays, temporarily lower music so dialogue is audible.