If you are creating a game project using the cocos2d XCode project template, there’s little reason not to use CocosDenshion for your audio needs. It’s already there, requires minimal configuration, it’s easy to use and it allows you to go as low-level as you need. One thing that CocosDenshion does not provide out of the box however is support for positional audio. Actually, the underlying audio API OpenAL does support full 3D audio, and you could get direct access to the API through CocosDenshion to take advantage of it, but for a 2D game this can be a major overkill. Positional audio can be simulated in 2D scenarios by simply manipulating the pan and gain of a sound source in relation to a listener, and that’s exactly what we’ll do in this piece of code that I’m about to share.

Where is this I hear?

The term itself is pretty self-explanatory, in that positional audio is the simulation of the ability that we humans have to judge the approximate distance and direction of a sound source, and also the physical properties of sound based on the environment. This effect is much better represented in surround sound systems with dedicated hardware but it can be “faked” in stereo systems using much simpler tricks.

CDXAudioNode

In keeping with the naming convention used in the CocosDenshion project, for our class we’ll use the CDX prefix to indicate that it depends on cocos2d (even though CocosDenshion comes with the cocos2d package, it’s entirely independent). We’ll be subverting the cocos2d API a little bit in order to use a CCNode to represent a sound source in our scene. This provides many advantages to us, such as adding the audio node as the child of a sprite, or even animating a sound source’s position using a CCAction, and you can also provide another CCNode to act as the listener position of a sound source (if none is provided, the default is the center of the screen). Another feature of the CDXAudioNode is to set a loop mode, including a “periodic” loop which can be very useful for ambient audio. We’ll see all of this in detail below.

  1. /**
  2.  Initializes the audio node with an already created sound buffer, identified by
  3.  the sourceId.
  4.  */
  5. - (id)initWithSoundEngine:(CDSoundEngine *)se sourceId:(int)sId;
  6.  
  7. /**
  8.  Initializes the audio node with an audio file, creating a sound buffer with the
  9.  specified sourceId.
  10.  */
  11. - (id)initWithFile:(NSString *)file soundEngine:(CDSoundEngine *)se sourceId:(int)sId;
/**
 Initializes the audio node with an already created sound buffer, identified by
 the sourceId.
 */
- (id)initWithSoundEngine:(CDSoundEngine *)se sourceId:(int)sId;

/**
 Initializes the audio node with an audio file, creating a sound buffer with the
 specified sourceId.
 */
- (id)initWithFile:(NSString *)file soundEngine:(CDSoundEngine *)se sourceId:(int)sId;

There are two ways to create a CDXAudioNode: you can either load the audio buffers yourself and just provide the source ID, or you can have the node load it’s own audio buffer by providing a file name. The former is useful in cases where you’re loading the audio buffers asynchronously or want to have more control over the loading process. Both of these constructors depend on having a CDSoundEngine already created and properly initialized.

  1. - (void)visit {
  2.     CGPoint realPos = [self convertToWorldSpace:CGPointZero];
  3.  
  4.     CGSize size = [[CCDirector sharedDirector] winSize];
  5.  
  6.     CGPoint earPos = ccp(size.width / 2, size.height / 2);
  7.     if (earNode) {
  8.         earPos = [earNode convertToWorldSpace:CGPointZero];
  9.     }
  10.  
  11.     float dist = sqrt((realPos.x - earPos.x) * (realPos.x - earPos.x) + (realPos.y - earPos.y) * (realPos.y - earPos.y));
  12.  
  13.     float distX = realPos.x - earPos.x;
  14.  
  15.     sound.pan = distX / (size.width / 2);
  16.  
  17.     float gain = 1.0f - (dist * attenuation);
  18.  
  19.     if (gain < 0.0f) gain = 0.0f;     if (gain > 1.0f) gain = 1.0f;
  20.  
  21.     sound.gain = gain;
  22.  
  23.     [super visit];
  24. }
- (void)visit {
    CGPoint realPos = [self convertToWorldSpace:CGPointZero];

    CGSize size = [[CCDirector sharedDirector] winSize];

    CGPoint earPos = ccp(size.width / 2, size.height / 2);
    if (earNode) {
        earPos = [earNode convertToWorldSpace:CGPointZero];
    }

    float dist = sqrt((realPos.x - earPos.x) * (realPos.x - earPos.x) + (realPos.y - earPos.y) * (realPos.y - earPos.y));

    float distX = realPos.x - earPos.x;

    sound.pan = distX / (size.width / 2);

    float gain = 1.0f - (dist * attenuation);

    if (gain < 0.0f) gain = 0.0f;     if (gain > 1.0f) gain = 1.0f;

    sound.gain = gain;

    [super visit];
}

All the magic happens in the visit method. This method is usually called every frame to perform drawing operations in a CCNode, but in this case we are hijacking it to calculate the distance between the ear node (or the center of the screen if it was not defined) and the sound source and then applying the necessary changes to the audio parameters.

Usage

  1. // Sound initialization
  2.  
  3. [CDSoundEngine setMixerSampleRate:CD_SAMPLE_RATE_MID];
  4. [CDAudioManager initAsynchronously:kAMM_FxPlusMusicIfNoOtherAudio];
  5.  
  6. // Waits for initialization (BAD way to do it, used here for simplicity's sake!)
  7. while ([CDAudioManager sharedManagerState] != kAMStateInitialised) {}
  8.  
  9. am = [CDAudioManager sharedManager];
  10. soundEngine = [CDAudioManager sharedManager].soundEngine;
  11.  
  12. ...
  13.  
  14. CDXAudioNode *audioNode = [CDXAudioNode audioNodeWithFile:@"808_120bpm.caf" soundEngine:soundEngine sourceId:1];
  15. audioNode.earNode = earSprite;
  16. audioNode.playMode = kAudioNodeLoop;
  17. [audioNode play];
// Sound initialization

[CDSoundEngine setMixerSampleRate:CD_SAMPLE_RATE_MID];
[CDAudioManager initAsynchronously:kAMM_FxPlusMusicIfNoOtherAudio];

// Waits for initialization (BAD way to do it, used here for simplicity's sake!)
while ([CDAudioManager sharedManagerState] != kAMStateInitialised) {}

am = [CDAudioManager sharedManager];
soundEngine = [CDAudioManager sharedManager].soundEngine;

...

CDXAudioNode *audioNode = [CDXAudioNode audioNodeWithFile:@"808_120bpm.caf" soundEngine:soundEngine sourceId:1];
audioNode.earNode = earSprite;
audioNode.playMode = kAudioNodeLoop;
[audioNode play];

After initializing the sound engine, you create your CDXAudioNode using one of the constructors mentioned before. Then you can define a listener using the earNode property and also set the play mode, which can have one of the following values:

  • kAudioNodeSinglePlay – the sound will be played once the play method is called.
  • kAudioNodeLoop – the sound will loop indefinitely, until the pause or stop methods are called.
  • kAudioNodePeriodicLoop – in this mode, the sound will play again every x seconds, where x is a random number between minLoopFrequency and maxLoopFrequency.

You can also play around with the attenuation property, which influences the maximum distance the sound source can be heard from.

Sample project

EDIT: the code is now available on GitHub

In this sample project, there are 4 sprites which you can drag around, where one is the listener node and the others are sound sources illustrating each of the loop modes available. The code can probably be much inproved, so feel free to use it in your own projects or improve it as needed :)