home *** CD-ROM | disk | FTP | other *** search
Text File | 1995-06-14 | 12.2 KB | 302 lines | [TEXT/R*ch] |
- // SoundEffects.c & SoundEffects.h
- // (C) 1994 Stuart Cheshire <cheshire@cs.stanford.edu>
- //
- // This code may be freely used for any non commercial programs and for
- // shareware software priced up to US$25 per copy. For other uses please
- // contact the author for permission.
- //
- // This sample code illustrates some of the tricks needed to get reliable
- // results out of the Macintosh Sound Manager.
- //
- // The major pitfall that Mac programmers will face is the ability to "kill"
- // the Sound Manager by calling it too rapidly -- this code shows how to
- // avoid that problem.
- //
- // This happens frequently when a game programmer calls the Sound Manager to
- // play a sound, and then some other event happens which requires a different
- // sound effect. The programmer then naturally follows the instructions in
- // Inside Macintosh and the technical notes, and sends a flushCmd and a
- // quietCmd to abort the sound already playing, waits for scChannelBusy to
- // clear, and then sends a bufferCmd to play the new sound. The problems are:
- // 1. If the new sound comes very quickly after the old one, so that the old
- // sound has NOT ACTUALLY STARTED PLAYING YET, then the attempt to cancel
- // it will make the Sound Manager get its pointers in knot and drop dead.
- // 2. Having to "wait for scChannelBusy to clear" can take a long time, and it
- // is anathema to any game programmer to be burning precious CPU cycles
- // spinning in a loop waiting for something to happen.
- //
- // The solution to (1) is:
- // Do not try to cancel a sound until it has been playing for at least 130ms.
- // The solution to (2) is:
- // Don't sit and wait in a loop. Continue with the game, and periodically
- // come back and check to see if the sound channel is ready yet.
- //
- // This sample code does both of these things, and has other nice touches,
- // such as adjusting the number of sound channels according to the CPU speed.
- //
- // The Macintosh Sound Manager is pretty nice, it is just a pity it can't
- // cope when you push it hard. It took me one day to put sound into Bolo,
- // and six months to get it debugged.
- //
- // Another big disappointment is that (unlike the Device Manager, the
- // File Manager, network drivers etc., etc., etc.) you can't call the
- // Sound Manager safely from completion routines. If I could then I could
- // make a really solid network telephone program where the sound wouldn't
- // break up no matter what else you were doing on the Mac at the time.
- // As it is, if you pull down a menu and hold the mouse button down,
- // sound programs on the Mac stop making sound after a couple of seconds.
- //
-
- #include <Sound.h>
- #include "StuTypes.h"
- #include "SoundEffects.h"
-
- typedef struct
- {
- SndChannel chan; // The sound manager workspace for this channel
- Handle soundhandle; // The sound we are playing
- long priority; // Lower priority values outrank higher ones
- long starttime; // Time when we started playing this sound
- u_char volume; // The volume level to play it at
- u_char killsound; // Set if we want an old sound to be aborted
- u_char makesound; // Set if we want a sound to be made on this channel
- u_char busy; // Set if the system reports this sound channel 'busy'
- } mySndChannel;
-
- #define MAX_SOUND_CHANNELS 8
- static mySndChannel *sound_channel = NULL;
- static mySndChannel *end_sound_channels = NULL;
-
- // **************************************************************************
- //
- // At application launch, allocate some memory to hold the sound channel state
- //
- void init_sounds(void)
- {
- SysEnvRec sysenvirons;
- SysEnvirons(1, &sysenvirons);
- if (sysenvirons.systemVersion < 0x607) return;
-
- sound_channel =
- (mySndChannel*) NewPtrClear(sizeof(mySndChannel) * MAX_SOUND_CHANNELS);
- end_sound_channels = sound_channel;
- if (!sound_channel) return;
-
- // Load sound resources into memory here, if you want to
- }
-
- // **************************************************************************
- //
- // At application exit, close the sound channels and free the memory
- //
- void quit_sounds(void)
- {
- if (sound_channel)
- {
- close_sound_channels(FALSE); // close ALL sound channels
- DisposPtr((Ptr)sound_channel);
- }
- }
-
- // **************************************************************************
- //
- // When the application wishes to make sounds, the sound channels should be
- // opened. This routine should be called in response to the application being
- // brought to the front layer (i.e. a MultiFinder 'resume' event) or when the
- // user selects a "Turn On Sound Effects" menu item.
- //
- // open_sound_channels will open as many sound channels as it can without
- // consuming too much CPU time, up to a maximum defined by MAX_SOUND_CHANNELS
- // (currently 8). I define "too much CPU time" as (65 - 5n)%, ie it will open
- // two sound channels if it can be done with 55% CPU, three in 50%, four in
- // 45%, five in 40%, etc. The intention is to ensure that slow machines get
- // a small number of channels which use a sensible amount of CPU time, and
- // faster machines get more sound channels. The reason for the sliding scale
- // is that setting a fixed limit of 20% would mean that slower Macs got no
- // sound channels at all, and a fixed limit of 60% would mean that a PowerMac
- // 8100 would end up opening about 57 sound channels.
- //
- void open_sound_channels(void)
- {
- short num_sound_channels = 0;
- while (end_sound_channels < &sound_channel[MAX_SOUND_CHANNELS])
- {
- SndChannelPtr sp = &end_sound_channels->chan;
- sp->qLength = stdQLength;
-
- // except for first channel, check CPU loading
- if (end_sound_channels > &sound_channel[0])
- {
- SndCommand myCmd;
- myCmd.cmd = totalLoadCmd;
- myCmd.param1 = 0;
- myCmd.param2 = initMono;
- // stop if we would exceed CPU usage limit
- // by allocating this sound channel
- if (SndControl(sampledSynth, &myCmd)) break;
- if (myCmd.param1 > 60 - num_sound_channels * 5) break;
- if (FreeMem() < 0x5000L) break; // if < 20K left, stop now.
- }
- if (SndNewChannel(&sp, sampledSynth, initMono, NULL)) break;
- end_sound_channels++;
- num_sound_channels++;
- }
- }
-
- // **************************************************************************
- //
- // When the application wishes to stop making sounds, the sound channels
- // should be closed. This routine should be called in response to the
- // application being put in the background (i.e. a MultiFinder 'suspend'
- // event) or when the user selects a "Turn Off Sound Effects" menu item.
- // If you wish to have your program (game?) continue making sounds in the
- // background then call close_sound_channels with keep_some=TRUE and it
- // will keep a single sound channel open. Keeping more than one channel
- // open when in the background would probably not be in the spirit of
- // 'cooperative' multitasking.
- //
- void close_sound_channels(Boolean keep_some)
- {
- mySndChannel *stop = &sound_channel[0];
- // If we want background sound, then leave one sound channel open
- if (keep_some) stop = &sound_channel[1];
- while (end_sound_channels > stop)
- SndDisposeChannel(&(--end_sound_channels)->chan, TRUE);
- }
-
- // **************************************************************************
- //
- // makesound is what the program calls when it wants to make a sound.
- // Handle s is a handle to the 'snd ' resource to be played.
- // u_short priority indicates the relative importance of different sounds.
- // SND_TYPE t indicates what action to take
- // if the sound is already playing
- // u_char volume is the volume (0-255) at which the sound is to be played
- //
- // The priorities allow some sounds to replace others. E.g. If a priority 2
- // sound is playing and a priority 1 sound happens, then if there are no
- // free sound channels left, the priority 2 will be interrupted to play the
- // priority 1 sound instead. If a priority 2 sound happens when all the
- // sound channels are busy playing priority 1 sounds, then the priority 2
- // sound will be ignored.
- //
- // Some sounds, like shots fired, are individual sounds in their own right.
- // Other sounds like running water, are intended to be a single continuous
- // sound. The SND_TYPE indicates what action to take if that sound is already
- // playing:
- // SND_INDIVIDUAL: Play a new independent sound
- // SND_REPLACE: Halt the currently playing sound and restart it
- // SND_NOREPLACE: Let the existing sound continue and ignore the new one
- //
- // The volume level is used in Bolo to make distant sounds quieter.
-
- #define sound_age(X) (timenow - (X)->starttime)
- #define weighted_priority(X) ((X)->priority + (sound_age(X) << 2))
- #define MIN_SAFE_SOUND_AGE 8
-
- #define my_chan_busy(X) ((X)->killsound || (X)->makesound || (X)->busy)
-
- void makesound(Handle s, u_short priority, SND_TYPE t, u_char volume)
- {
- mySndChannel *i = end_sound_channels; // Start with my "not found" value
- mySndChannel *bestslot = &sound_channel[0];
- long timenow = TickCount();
- u_char interrupt = TRUE;
-
- if (!s || !*s) return;
-
- // For special sounds, see if sound is already playing, and if so,
- // decide whether to replace it or ignore it
- if (t != SND_INDIVIDUAL)
- for (i=&sound_channel[0]; i<end_sound_channels; i++)
- if (i->soundhandle == s && my_chan_busy(i))
- {
- if (t == SND_REPLACE && sound_age(i) >= MIN_SAFE_SOUND_AGE)
- break;
- else return;
- }
-
- // If not special sound, or special sound not found,
- // try to find a free channel
- if (i == end_sound_channels)
- for (i=&sound_channel[0]; i<end_sound_channels; i++)
- {
- // If channel not busy, use it
- if (!my_chan_busy(i)) { interrupt = FALSE; break; }
- // make sure sound is old enough to safely interrupt it
- if (sound_age(i) >= MIN_SAFE_SOUND_AGE &&
- weighted_priority(i) > weighted_priority(bestslot))
- bestslot = i;
- }
-
- // If still haven't found a channel to use, see if we can steal another
- if (i == end_sound_channels)
- {
- if (priority > weighted_priority(bestslot) ||
- sound_age(bestslot) < MIN_SAFE_SOUND_AGE) return;
- else i = bestslot;
- }
-
- // OK, now fill in the details for the service routine to act on
- i->soundhandle = s;
- i->priority = priority;
- i->starttime = timenow;
- i->volume = volume;
- i->killsound = interrupt;
- i->makesound = TRUE;
- }
-
- // **************************************************************************
- //
- // service_sound_channels must be called periodically, say each time around
- // your main event loop, or, if you want to be really bold, from a 60Hz VLB
- // task. The Sound Manager is not particularly stable when called from VBL
- // tasks or other interrupt routines. It won't crash the Mac, but it may
- // make garbage squawking sounds occasionally, so it may help to disable
- // interrupts as indicated below.
- //
- void service_sound_channels(void)
- {
- mySndChannel *i;
- // DISABLE_INTERRUPTS ?
- for (i=&sound_channel[0]; i<end_sound_channels; i++)
- {
- SCStatus ss;
- SndChannelStatus(&i->chan, sizeof(ss), &ss);
- i->busy = (ss.scChannelBusy != 0);
-
- if (i->killsound)
- {
- SndCommand myCmd;
- myCmd.cmd = flushCmd;
- myCmd.param1 = 0;
- myCmd.param2 = 0;
- SndDoImmediate(&i->chan, &myCmd);
-
- myCmd.cmd = quietCmd;
- myCmd.param1 = 0;
- myCmd.param2 = 0;
- SndDoImmediate(&i->chan, &myCmd);
-
- i->killsound = FALSE;
- }
- else if (i->makesound && !i->busy)
- {
- SndCommand myCmd;
- myCmd.cmd = ampCmd;
- myCmd.param1 = i->volume;
- myCmd.param2 = 0;
- SndDoImmediate(&i->chan, &myCmd);
-
- myCmd.cmd = bufferCmd;
- myCmd.param1 = 0;
- myCmd.param2 = (long)(*i->soundhandle) + 0x14;
- SndDoImmediate(&i->chan, &myCmd);
-
- i->makesound = FALSE;
- i->busy = TRUE; // Yes, we have now made a sound
- }
- }
- // RESTORE_INTERRUPTS ?
- }
-