Sonix Programmer's Guide

Juggler Simple Sound Interface.

$Date: 2002/04/23 16:19:30 $


Table of Contents

Introduction
Simple Minimalist Interface
Reconfiguration
Design and Implementation of Sonix
Design Patterns Overview
Pluggable audio subsystems
References

Introduction

Sonix provides simple audio sound objects on top of several audio APIs. The interface to Sonix is kept very simple in order to get people up and running with sound as fast as possible.

Here is an overview of Sonix capabilities:

  • Provides simple access to spacialized sound triggering.

  • Small learning curve, Simple interface and usage.

  • Runs on top of several well known audio systems—currently OpenAL and Multigen/Paradigm's AudioWorks.

  • Supports reconfigurability at runtime.

    Reconfigurations of sound resources are protected (i.e. reconfiguration does not break application).

    Changing a sound resource reflects properly in all other handles to the same resource (resources allow multiple users).

  • Supports features needed by 3D virtual environments.

    Spacialized or ambient audio.

    One-shot and looping sounds.

Simple Minimalist Interface

Figure 1. UML diagram of the Sonix interface.

UML diagram of the Sonix interface.

In Figure 1, “UML diagram of the Sonix interface.” we see the API for Sonix (see Figure 2, “The Sonix design.” for the complete software architecture). The main parts that a sound programmer will use are shown, namely the snx::SoundHandle class, and the sonix singleton class. The sonix singleton class is used to start, stop and reconfigure the sound system. The snx::SoundHandle class is used to manipulate individual sounds. Both classes must be used for any sound to be heard.

Starting the Sonix system is easy (see Example 1, “Code to startup and initialize Sonix to use the OpenAL audio subsystem”). Basically the only call needed to start the system is changeAPI(). Currently only OpenAL and AudioWorks are supported by Sonix, so you would pass one of these two strings to changeAPI().

Example 1. Code to startup and initialize Sonix to use the OpenAL audio subsystem

sonix::instance()->changeAPI( "OpenAL" );

To setup a sound is also straightforward as seen in Example 2, “Code to setup a Sonix sound”. Here we use a snx::SoundInfo to configure the sound object, which is accessed by a snx::SoundHandle.

Example 2. Code to setup a Sonix sound

snx::SoundInfo info;
info.filename = "crack.wav";
info.datasource = snx::SoundInfo::FILESYTEM;

snx::SoundHandle crack_sound( "crack" );
crack_sound.configure( info );

To keep Sonix running, you need to repeatedly call an update function called step( float time_delta ). time_delta is the amount of time since you last called step(), and step() should be called in your application's frame function (see Example 3, “Call sonix::step() in your frame function”).

Example 3. Call sonix::step() in your frame function

void frame()
{
   time_delta = getTimeChangeInSeconds(); // use a system call, or 
                                          // other API to get your time delta
   sonix::instance()->step( time_delta );
}

Example 4. Example C++ program that uses Sonix to play a sound using OpenAL

#include <iostream>
#include <string>
#include <snx/sonix.h>

int main( int argc, char* argv[] )
{
   std::string filename( "808kick.wav" ), api( "OpenAL" );
   
   if (!snxFileIO::fileExists( filename.c_str() ))
   {
      std::cout << "File not found: " << filename << "\n" << std::flush;
      return 0;
   }

   // start sonix using OpenAL
   sonix::instance()->changeAPI( api );
      
   // fill out a description for the sound we want to play
   snx::SoundInfo sound_info;
   sound_info.filename = filename;
   sound_info.datasource = snx::SoundInfo::FILESYSTEM;

   // create the sound object
   snx::SoundHandle sound_handle;
   sound_handle.init( "my simple sound" );
   sound_handle.configure( sound_info );
   
   // trigger the sound
   sound_handle.trigger();
   sleep( 1 );

   // trigger the sound from a different position in 3D space...
   sound_handle.setPosition( 10.0f, 0.0f, 0.0f );
   sound_handle.trigger();
   sleep( 1 );

   // this simulates a running application...
   while (1)
   {
      sonix::instance()->step( time_delta );    
   }

   return 1;
}

Reconfiguration

Sonix is reconfigurable allowing audio APIs to be safely swapped out at runtime without the dependent systems noticing. Application using Sonix expect to be completely portable. Changing sound APIs at run time can be useful so that the user can experiment with quality and latency differences of different hardware and sound APIs.

If no audio API is available on a given platform, your calls to Sonix simply are ignored. This gives the benefit that you need no special code in your application to enable or disable sound—it is handled by Sonix.

The benefit of reconfiguration is that when something changes, your application code does not need to be aware or do any special handling. Everything in Sonix is changeable behind the scenes during application execution. See Example 5, “How to reconfigure Sonix at runtime” for an example of how to reconfigure Sonix in C++. This probably makes more sense in a more complex application where there is some application logic that triggers sounds and does not want or need to know when a sound has changed state, or when the user has tried out a new audio API.

Example 5. How to reconfigure Sonix at runtime

   // start sonix using OpenAL
   sonix::instance()->changeAPI( "OpenAL" );
      
   // fill out a description for the sound we want to play
   snx::SoundInfo sound_info;
   sound_info.filename = "808kick.wav";
   sound_info.datasource = snx::SoundInfo::FILESYSTEM;

   // create the sound object
   snx::SoundHandle sound_handle;
   sound_handle.init( "my sound for testing" );
   sound_handle.configure( sound_info );
   
   // trigger the sound
   sound_handle.trigger();
   sleep( 1 );

   // trigger the sound using a different audio system...
   sonix::instance()->changeAPI( "AudioWorks" );
   sound_handle.trigger();
   sleep( 1 );

   // trigger our sound object using different source data
   sound_info.filename = "303riff.wav";
   sound_handle.configure( sound_info );
   sound_handle.trigger();
   sleep( 1 );

Design and Implementation of Sonix

Figure 2. The Sonix design.

The Sonix design.

Sonix was designed (see Figure 2, “The Sonix design.”) using modern software design principles including design patterns and object oriented design.

Design Patterns Overview

Design patterns describe simple and elegant solutions to specific problems in object oriented software design [Patterns]. When designing Sonix, we used many design patterns, which were appropriate to a simple audio system [Gems2].

  • Adapter (snx::SoundImplementation). This adapter provides a common interface to the underlying sound API.

  • Prototype (snx::SoundImplementation). Making snx::SoundImplementation a Prototype allows a new cloned object to be created from it that has duplicate state.

  • Store/plugin-method (snx::SoundFactory). Each sound implementation is registered with a Store called snx::SoundFactory. This Store allows users to select items from its inventory. Another name for Store is "Abstract Factory".

  • Abstract Factory (snx::SoundFactory). The Store can create new instances of the requested sound implementation. The Abstract Factory consults its Store of registered objects, and if found, makes a clone of that object (Prototype pattern). The Abstract Factory is used in Sonix to configure the Bridge.

  • Bridge (sonix interface class and snx::SoundImplementation). The sonix class is the audio system abstraction which is decoupled from its implementation snx::SoundImplementation. This way the two can vary independently. Bridge also facilitates run-time configuration of the sound API.

  • Proxy (std::string and snx::SoundHandle). snx::SoundHandle is how users manipulate their sound object. snx::SoundHandle is actually a proxy to a std::string proxy. The std::string Proxy is what allows Sonix reconfiguration of resources. Rather than using pointers which can easily be left to dangle, the std::string serves as a lookup for a protected sound resource located internally to the Sonix tool. The snx::SoundHandle wraps this std::string to provide a simple and familiar C++ object to use as the sound handle. The Sonix class acts as Mediator between every Proxy method and the actual audio system Adapter.

Pluggable audio subsystems

Figure 3. How Sonix is able to support many audio subsystems through "plug-ins"

How Sonix is able to support many audio subsystems through "plug-ins"

Sonix supports the selection of several audio subsystems by the application through implementation plug-ins (see Figure 3, “How Sonix is able to support many audio subsystems through "plug-ins"”). Each plug-in implements an adapter to an underlying audio subsystem. The adapter supports a common interface that Sonix knows how talk to. Each adapter is then registered with a factory object, which may ask that adapter to clone itself for use by whoever called the factory.

References

[Gems2] Scott Patterson. “Game Audio Design Patterns”. 514. Ian Lewis. “A Low-Level Sound API”. 559 . Mark Deloura. Game Programming Gems 2. Charles River Media . 2001. Copyright © 2001 Charles River Media Inc..

[Patterns] Erich Gamma. Richard Helm. Ralph Johnson. John Vlissides. Design Patterns. Elements of Reusable Object-Oriented Software. Addison Wesley . 1995. Copyright © 1995 Addison Wesley Longman, Inc.