Table of Contents
- Audio System
- Overview
- Architecture
- Dynamic Tracks
- Playing Audio
- Track Management
- Volume Control
- Spatial Audio
- Audio Effects (DSP)
- Music Containers
- Actor Audio Integration
- Music Streaming
- Audio Bank
- Pitch Shifting
- Music Playlists
- Common Patterns
- Audio Format Requirements
- Performance Considerations
- Best Practises
- Troubleshooting
- See Also
Audio System
Terrarum features a comprehensive audio engine with spatial sound, mixer buses, real-time effects processing, and advanced music playback capabilities.
Overview
The audio system provides:
- AudioMixer — Central audio management with mixer buses
- Spatial audio — 3D positional sound
- Dynamic tracks — Multiple simultaneous music/sound channels
- Effects processing — Filters, reverb, panning, and more
- Music streaming — Efficient playback of large music files
- Volume control — Master, music, SFX, and UI volume
Architecture
AudioMixer
The AudioMixer singleton manages all audio output:
object App {
val audioMixer: AudioMixer
}
The mixer runs on a separate thread (AudioManagerRunnable) for low-latency audio processing.
Audio Codex
All audio assets are registered in the AudioCodex:
object AudioCodex {
operator fun get(audioID: String): MusicContainer?
}
Dynamic Tracks
The mixer provides multiple dynamic audio tracks:
val dynamicTracks: Array<TerrarumAudioMixerTrack>
Each track can play different audio with independent:
- Volume control
- Effects processing (filters)
- Panning
- Pitch shifting
Track Count
The number of available tracks depends on configuration, typically:
- Music tracks: 4-8 simultaneous music streams
- SFX tracks: 16-32 sound effects
Playing Audio
Music Playback
fun startMusic(music: MusicContainer, track: Int = 0) {
val mixerTrack = App.audioMixer.dynamicTracks[track]
mixerTrack.setMusic(music)
mixerTrack.play()
}
Sound Effects
Short sound effects use the same track system:
fun playSound(sound: MusicContainer, volume: Float = 1.0f) {
val track = App.audioMixer.getAvailableTrack()
track.setMusic(sound)
track.trackVolume = TrackVolume(volume, volume)
track.play()
}
Track Management
TerrarumAudioMixerTrack
class TerrarumAudioMixerTrack {
var trackVolume: TrackVolume
var filters: Array<AudioFilter>
var trackingTarget: ActorWithBody? // For spatial audio
fun play()
fun stop()
fun pause()
fun resume()
}
Track States
- Stopped — Not playing
- Playing — Currently playing
- Paused — Paused, can resume
- Stopping — Fading out
Volume Control
TrackVolume
Stereo volume control:
class TrackVolume(
var left: Float, // 0.0-1.0
var right: Float // 0.0-1.0
)
Master Volume
Global volume settings:
App.audioMixer.masterVolume // Overall volume
App.audioMixer.musicVolume // Music volume multiplier
App.audioMixer.sfxVolume // SFX volume multiplier
App.audioMixer.uiVolume // UI sounds volume multiplier
Final track volume = trackVolume * categoryVolume * masterVolume
Spatial Audio
Positional Sound
Attach sound to an actor for 3D positioning:
mixerTrack.trackingTarget = actor
The mixer automatically:
- Calculates distance from listener
- Applies distance attenuation
- Calculates stereo panning
- Updates in real-time as actor moves
3D Audio Calculation
val listener = playerActor // Usually the player
val source = trackingTarget
val distance = source.position.dst(listener.position)
val angle = atan2(
source.position.y - listener.position.y,
source.position.x - listener.position.x
)
// Distance attenuation
val attenuation = 1.0f / (1.0f + distance / referenceDistance)
// Stereo panning
val pan = sin(angle) // -1.0 (left) to 1.0 (right)
trackVolume.left = attenuation * (1.0f - max(0.0f, pan))
trackVolume.right = attenuation * (1.0f + min(0.0f, pan))
Listener Position
The audio listener is typically the player's position:
App.audioMixer.listenerPosition = player.hitbox.center
Audio Effects (DSP)
Filter System
Each track has two filter slots:
val filters: Array<AudioFilter> = arrayOf(NullFilter, NullFilter)
Built-in Filters
- NullFilter — No effect (bypass)
- BinoPan — Binaural panning
- LowPassFilter — Reduces high frequencies
- HighPassFilter — Reduces low frequencies
- ReverbFilter — Room ambience
- EchoFilter — Delay effect
Applying Filters
import net.torvald.terrarum.audio.dsp.LowPassFilter
mixerTrack.filters[0] = LowPassFilter(cutoffFreq = 1000.0f)
mixerTrack.filters[1] = ReverbFilter(roomSize = 0.5f, damping = 0.7f)
Filter Chaining
Filters process in order:
Audio Source → Filter[0] → Filter[1] → Output
Removing Filters
mixerTrack.filters[0] = NullFilter
Music Containers
MusicContainer
Wraps audio assets for playback:
class MusicContainer(
val file: FileHandle,
val format: AudioFormat
)
enum class AudioFormat {
OGG_VORBIS,
WAV,
MP3
}
Loading Music
val music = MusicContainer(
Gdx.files.internal("audio/music/theme.ogg"),
AudioFormat.OGG_VORBIS
)
AudioCodex.register("theme_music", music)
Actor Audio Integration
Actors can manage their own audio:
abstract class Actor {
val musicTracks: HashMap<MusicContainer, TerrarumAudioMixerTrack>
fun startMusic(music: MusicContainer)
fun stopMusic(music: MusicContainer)
}
Automatic Cleanup
When actors despawn, their audio stops automatically:
override fun despawn() {
if (stopMusicOnDespawn) {
musicTracks.forEach { (_, track) ->
track.stop()
}
}
}
Music Streaming
Large music files are streamed rather than loaded entirely:
class MusicStreamer(
val file: FileHandle,
val bufferSize: Int = 4096
) {
fun readNextBuffer(): ByteArray
}
This allows playback of long music tracks without excessive memory usage.
Audio Bank
The AudioBank manages audio asset loading and caching:
object AudioBank {
fun loadMusic(path: String): MusicContainer
fun unloadMusic(musicID: String)
}
Pitch Shifting
Tracks support pitch adjustment:
mixerTrack.processor.streamBuf?.pitch = 1.5f // 1.5x speed (higher pitch)
Pitch range: 0.5 (half speed) to 2.0 (double speed).
Music Playlists
class TerrarumMusicPlaylist {
val tracks: List<MusicContainer>
var currentIndex: Int
var shuffleMode: Boolean
fun next()
fun previous()
fun shuffle()
}
Playlists handle music progression and looping, and allows audio processor to fetch the next track in gapless manner.
Common Patterns
Background Music
fun playBackgroundMusic(musicID: String) {
val music = AudioCodex[musicID]
val track = App.audioMixer.dynamicTracks[0] // Reserve track 0 for BGM
track.setMusic(music)
track.trackVolume = TrackVolume(
App.audioMixer.musicVolume,
App.audioMixer.musicVolume
)
track.play()
}
UI Click Sound
fun playUIClick() {
val sound = AudioCodex["ui_click"]
val track = App.audioMixer.getAvailableTrack()
track.setMusic(sound)
track.trackVolume = TrackVolume(
App.audioMixer.uiVolume,
App.audioMixer.uiVolume
)
track.play()
}
Positional Ambient Sound
fun playAmbientSound(position: Vector2, sound: MusicContainer) {
val track = App.audioMixer.getAvailableTrack()
// Create temporary actor at position for spatial audio
val soundSource = object : ActorWithBody() {
init {
setPosition(position.x, position.y)
}
}
track.trackingTarget = soundSource
track.setMusic(sound)
track.play()
}
Muffled Sound (Underwater Effect)
fun applyUnderwaterEffect(track: TerrarumAudioMixerTrack) {
track.filters[0] = LowPassFilter(cutoffFreq = 500.0f)
track.filters[1] = ReverbFilter(roomSize = 0.8f, damping = 0.9f)
}
Audio Format Requirements
Music Files
- Format: OGG Vorbis recommended
- Quality:
-q 10(highest quality) - Sample rate: 48000 Hz
- Channels: Stereo
Sound Effects
- Format: OGG Vorbis or WAV
- Duration: < 5 seconds (use streaming for longer)
- Sample rate: 48000 Hz
- Channels: Mono (will be positioned) or Stereo (will also be positioned but poorly)
Performance Considerations
- Limit simultaneous sounds — Too many tracks cause CPU overhead
- Use streaming for music — Don't load entire files
- Unload unused audio — Free memory when not needed
- Pool sound effect tracks — Reuse tracks instead of creating new ones
- Reduce filter complexity — Heavy DSP effects impact performance
Best Practises
- Reserve tracks for categories — E.g., track 0 for BGM, 1-3 for ambient
- Stop music on despawn — Prevent audio leaks
- Use spatial audio sparingly — Not all sounds need positioning
- Normalise audio levels — Ensure consistent volume across files
- Test with volume at 0 — Game should work without audio
- Provide audio toggles — Let users disable categories
- Crossfade music transitions — Avoid abrupt cuts
Troubleshooting
No Sound
- Check master volume settings
- Verify audio files are loaded
- Ensure OpenAL is initialised
- Check track availability
Crackling/Popping
- Increase buffer size
- Reduce simultaneous tracks
- Check for audio file corruption
- Verify sample rate consistency
Spatial Audio Not Working
- Ensure
trackingTargetis set - Verify listener position is updated
- Check distance attenuation settings