Under The Microscope

Playing Sound Effects the Right Way

When you use your Mac, you’re likely to hear a chorus of sound effects, from the chime of new email to the ding of a new IM. Mac OS X actually handles these sound effects differently from other audio content. Open the Sound System Preference, and you’ll see the Sound Effects tab, where users can set a distinct output device for sound effects. This is handy when a user has a second audio device (such as USB speakers) for playing music, but don’t want beeps overlaid on their audio. To avoid that, effects can be set to play through the built-in speaker, where they won’t cause a deafening interruption.

Doing It Right

Unfortunately, many applications do not currently play their sound effects properly, ignoring the user’s Sound Effects output selection. The end result for users is that audio snippets which are in fact sound effects incorrectly play through the main audio output. Luckily, fixing the problem is quite simple. Below, we show example code for playing back audio with Cocoa’s NSSound, on the system alert device.

// Alert Sound Playback Snippet
// With thanks to Guy English for initial draft of this code

#import <CoreAudio/CoreAudio.h>

@interface NSSound (AlertAudioAdditions)
+ (NSString*)_alertSoundAlertDeviceIdentifier;
+ (id)alertSoundNamed: (NSString*)name;
@end

@implementation NSSound (AlertAudioAdditions)

+ (NSString*)_alertSoundAlertDeviceIdentifier
{
AudioDeviceID deviceID = 0;
UInt32 size = sizeof( AudioDeviceID );

// get the system default audio output device …
AudioObjectPropertyAddress address = {
kAudioHardwarePropertyDefaultSystemOutputDevice,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMaster
};

OSStatus error = AudioObjectGetPropertyData( kAudioObjectSystemObject, &address, 0, NULL, &size, &deviceID );
if ( error != noErr ) return nil;

// … then, since one exists, get the unique identifier …
UInt32 stringSize = sizeof( CFStringRef );
CFStringRef deviceUniqueIdentifier = NULL;

address.mSelector = kAudioDevicePropertyDeviceUID;
address.mScope = kAudioObjectPropertyScopeGlobal;
address.mElement = kAudioObjectPropertyElementMaster;

error = AudioObjectGetPropertyData( deviceID, &address, 0, NULL, &stringSize, &deviceUniqueIdentifier );
if ( error != noErr ) return nil;

return [(NSString*) deviceUniqueIdentifier autorelease];
}

+ (id)alertSoundNamed: (NSString*)name
{
NSSound *sound = [NSSound soundNamed: name];
[sound setPlaybackDeviceIdentifier: [self _alertSoundAlertDeviceIdentifier]];
return sound;
}

@end

// example usage:
[[NSSound alertSoundNamed: @”Moof.aiff”] play]


Note: For code which doesn’t use NSSound, the key point is to use kAudioHardwarePropertyDefaultSystemOutputDevice to get the alert device id, and then perform playback through there.

Self-Interest

Getting this fixed is a good thing for all, but we do have a bit of self-interest here as well, tied to our new application Intermission. Intermission lets you pause and rewind audio on your Mac, but when it comes to sound effects we specifically work to not buffer those noises. To avoid the confusion of things like getting an alert for a “new” IM 30 minutes after it was received, Intermission passes sound effects through live at all times, instead of buffering them. This works great with Mail.app, Messages, Adium, and more. Currently, however, many third-party apps don’t behave quite so well. We’re hoping to convince developers to improve that.

Real-World Example

Napkin IconAs a quick test case, we worked with our friends at Aged & Distilled to get their visual communication tool Napkin updated. Former Rogue Amoeba Guy English spent just a few minutes updating the app’s sound playing code to getting it working as it should, with Sound Effects playing through the correct device and passing through Intermission unbuffered. Perfect!

Their next update will include this fix, and yours could too. Just check out the sample code above to get started.

Update (September 16th, 2013): As C-Command’s Michael Tsai points out, the more modern AudioServicesPlayAlertSound API (new in OS X 10.5!) will play sounds on the alert device and honor the user’s alert volume setting. For new code, it’s likely the best bet (while the above code may work better with existing NSSound-based code). Here’s a quick example of how to use it:

#import <AudioToolbox/AudioServices.h>

static void _AlertAudio_PlaybackCompleted( SystemSoundID soundID, void* clientData )
{
AudioServicesDisposeSystemSoundID( soundID );
}

BOOL AlertAudio_Play( NSString* name )
{
NSURL* url = [[NSBundle mainBundle] URLForResource: [name stringByDeletingPathExtension] withExtension: [name pathExtension]];

if( !url )
return NO;

SystemSoundID soundID;
OSStatus err = AudioServicesCreateSystemSoundID( (CFURLRef)url, &soundID );

if( err )
return NO;

(void)AudioServicesAddSystemSoundCompletion( soundID, NULL, NULL, _AlertAudio_PlaybackCompleted, NULL );

AudioServicesPlayAlertSound( soundID );

return YES;
}

AlertAudio_Play( @”Moof.aif” );

Leave a Reply

You must be logged in to post a comment.

Our Software