Raytraced Audio - Loading

Sector's Edge has hundreds of sounds - footsteps, explosions, gunfire - some of which have multiple variations to reduce repetitiveness. These sounds add up to 163mb of data and this article explains how they are loaded on background threads to reduce the game's startup time.

Background Loading

For each sound file that needs to be loaded, a SoundStorageVariation object is created, which manages loading and decoding the .ogg sound file.

The .NET Thread Pool is excellent at creating and executing background threads, so we simply schedule a Task that runs t_LoadFromDisk.

public partial class SoundStorageVariation
{
    SETask loadingTask;

    // This stores uint16 sound data, sample rate, format, etc
    SoundBufferData data;
    
    public SoundStorageVariation(string fileName)
    {
        loadingTask = TaskHelper.Run(() => t_LoadFromDisk(fileName), TaskType.LoadSound);
    }
    
    public void t_LoadFromDisk(string fileName)
    {
        // Load OGG data from disk and decode it
        data = AM.LoadOGG(fileName);
    }
}
The t_ prefix indicates the function will run on another thread

Multiple SoundStorageVariation objects are stored in a SoundStorage object, which is then stored in a dictionary called GlobalSoundStorage<SoundType, SoundStorage>. This lets us quickly access sound data by its type:

Audio Manager

As various parts of the game can play sounds (UI, networking, players, voxels), all sound-related functionality is stored in a public static class called AM, which is short for Audio Manager.

AM contains the GlobalSoundStorage dictionary, as well as another list called VariationsToUpload, which only stores the variations that need to be 'uploaded', i.e. read from disk and stored in an OpenAL buffer.

During each frame on the main thread the audio manager iterates over each SoundStorageVariation in the VariationsToUpload list, and copies the sound data to an OpenAL buffer if its loadingTask has finished executing. Only one variation is uploaded per frame to keep frame time as low as possible.

The audio manager is also responsible for playing, updating and disposing sounds, associating sounds with moving objects like grenades, and also handles audio raytracing. This is explored in the next articles in this series.

Lazy Execution

If the game tries to play a sound that hasn't been uploaded, it simply doesn't play the sound. Waiting for the background task to finish would cause the main thread to stall, especially if the task is deeper into the queue.

This is called 'lazy execution' and ensures the game never waits for a background thread to finish processing before continuing on. This prevents lag spikes and keeps the foreground and background threads independent.

Sector's Edge uses lazy execution as much as possible, offloading expensive operations onto background threads. For example if it takes 10ms to update all player animations, then the animation code is run on background threads at 100 frames per second (FPS) while the rest of the game updates at 200 FPS. This prevents the game from slowing down to 100 FPS just for player animations.

Custom Task Scheduler

When all background threads are running tasks, the default .NET TaskScheduler will run the next queued task on the main thread. This means it is not guaranteed that all scheduled tasks will execute on a background thread, especially when scheduling a lot of sound loading tasks on startup.

To prevent this from happening, the game uses its own TaskScheduler, which sets MaximumConcurrencyLevel to the amount of threads minus one.

public class SEScheduler : TaskScheduler
{
    static int maxThreads;
    
    static SEScheduler
    {
        // Get the amount of threads only once
        ThreadPool.GetMinThreads(out int workerThreads, out _);
        maxThreads = Math.Max(1, workerThreads - 1);
    }
    
    public override int MaximumConcurrencyLevel => maxThreads;
    
    ...
}

Now when all background threads are busy running tasks, the scheduler will not attempt to run the next queued task on the main thread.

Next Steps

The next articles in this series will cover:

  • linking OpenAL sound sources with moving game objects
  • directional ambience - calculating rolling averages of weighted vectors
  • multithreading - ensuring all heavy processing occurs on background threads
  • the raycasting function