1
Audio System
minjaesong edited this page 2025-11-24 21:24:45 +09:00

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:

  1. Calculates distance from listener
  2. Applies distance attenuation
  3. Calculates stereo panning
  4. 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

  1. Limit simultaneous sounds — Too many tracks cause CPU overhead
  2. Use streaming for music — Don't load entire files
  3. Unload unused audio — Free memory when not needed
  4. Pool sound effect tracks — Reuse tracks instead of creating new ones
  5. Reduce filter complexity — Heavy DSP effects impact performance

Best Practises

  1. Reserve tracks for categories — E.g., track 0 for BGM, 1-3 for ambient
  2. Stop music on despawn — Prevent audio leaks
  3. Use spatial audio sparingly — Not all sounds need positioning
  4. Normalise audio levels — Ensure consistent volume across files
  5. Test with volume at 0 — Game should work without audio
  6. Provide audio toggles — Let users disable categories
  7. 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 trackingTarget is set
  • Verify listener position is updated
  • Check distance attenuation settings

See Also