home *** CD-ROM | disk | FTP | other *** search
/ Tricks of the Mac Game Programming Gurus / TricksOfTheMacGameProgrammingGurus.iso / Information / Stuart's Tech Notes / Sound Manager Woes / SoundEffects.c next >
Encoding:
Text File  |  1995-06-14  |  12.2 KB  |  302 lines  |  [TEXT/R*ch]

  1. // SoundEffects.c & SoundEffects.h
  2. // (C) 1994 Stuart Cheshire <cheshire@cs.stanford.edu>
  3. //
  4. // This code may be freely used for any non commercial programs and for
  5. // shareware software priced up to US$25 per copy. For other uses please
  6. // contact the author for permission.
  7. //
  8. // This sample code illustrates some of the tricks needed to get reliable
  9. // results out of the Macintosh Sound Manager.
  10. //
  11. // The major pitfall that Mac programmers will face is the ability to "kill"
  12. // the Sound Manager by calling it too rapidly -- this code shows how to
  13. // avoid that problem.
  14. //
  15. // This happens frequently when a game programmer calls the Sound Manager to
  16. // play a sound, and then some other event happens which requires a different
  17. // sound effect. The programmer then naturally follows the instructions in
  18. // Inside Macintosh and the technical notes, and sends a flushCmd and a
  19. // quietCmd to abort the sound already playing, waits for scChannelBusy to
  20. // clear, and then sends a bufferCmd to play the new sound. The problems are:
  21. // 1. If the new sound comes very quickly after the old one, so that the old
  22. //    sound has NOT ACTUALLY STARTED PLAYING YET, then the attempt to cancel
  23. //    it will make the Sound Manager get its pointers in knot and drop dead.
  24. // 2. Having to "wait for scChannelBusy to clear" can take a long time, and it
  25. //    is anathema to any game programmer to be burning precious CPU cycles
  26. //    spinning in a loop waiting for something to happen.
  27. //
  28. // The solution to (1) is:
  29. // Do not try to cancel a sound until it has been playing for at least 130ms.
  30. // The solution to (2) is:
  31. // Don't sit and wait in a loop. Continue with the game, and periodically
  32. // come back and check to see if the sound channel is ready yet.
  33. //
  34. // This sample code does both of these things, and has other nice touches,
  35. // such as adjusting the number of sound channels according to the CPU speed.
  36. //
  37. // The Macintosh Sound Manager is pretty nice, it is just a pity it can't
  38. // cope when you push it hard. It took me one day to put sound into Bolo,
  39. // and six months to get it debugged.
  40. //
  41. // Another big disappointment is that (unlike the Device Manager, the
  42. // File Manager, network drivers etc., etc., etc.) you can't call the
  43. // Sound Manager safely from completion routines. If I could then I could
  44. // make a really solid network telephone program where the sound wouldn't
  45. // break up no matter what else you were doing on the Mac at the time.
  46. // As it is, if you pull down a menu and hold the mouse button down,
  47. // sound programs on the Mac stop making sound after a couple of seconds.
  48. //
  49.  
  50. #include <Sound.h>
  51. #include "StuTypes.h"
  52. #include "SoundEffects.h"
  53.  
  54. typedef struct
  55.     {
  56.     SndChannel chan;    // The sound manager workspace for this channel
  57.     Handle soundhandle; // The sound we are playing
  58.     long priority;      // Lower priority values outrank higher ones
  59.     long starttime;     // Time when we started playing this sound
  60.     u_char volume;      // The volume level to play it at
  61.     u_char killsound;   // Set if we want an old sound to be aborted
  62.     u_char makesound;   // Set if we want a sound to be made on this channel
  63.     u_char busy;        // Set if the system reports this sound channel 'busy'
  64.     } mySndChannel;
  65.  
  66. #define MAX_SOUND_CHANNELS 8
  67. static mySndChannel *sound_channel       = NULL;
  68. static mySndChannel *end_sound_channels  = NULL;
  69.  
  70. // **************************************************************************
  71. //
  72. // At application launch, allocate some memory to hold the sound channel state
  73. //
  74. void init_sounds(void)
  75.     {
  76.     SysEnvRec sysenvirons;
  77.     SysEnvirons(1, &sysenvirons);
  78.     if (sysenvirons.systemVersion < 0x607) return;
  79.  
  80.     sound_channel =
  81.        (mySndChannel*) NewPtrClear(sizeof(mySndChannel) * MAX_SOUND_CHANNELS);
  82.     end_sound_channels = sound_channel;
  83.     if (!sound_channel) return;
  84.     
  85.     // Load sound resources into memory here, if you want to
  86.     }
  87.  
  88. // **************************************************************************
  89. //
  90. // At application exit, close the sound channels and free the memory
  91. //
  92. void quit_sounds(void)
  93.     {
  94.     if (sound_channel)
  95.         {
  96.         close_sound_channels(FALSE);    // close ALL sound channels
  97.         DisposPtr((Ptr)sound_channel);
  98.         }
  99.     }
  100.  
  101. // **************************************************************************
  102. //
  103. // When the application wishes to make sounds, the sound channels should be
  104. // opened. This routine should be called in response to the application being
  105. // brought to the front layer (i.e. a MultiFinder 'resume' event) or when the
  106. // user selects a "Turn On Sound Effects" menu item.
  107. //
  108. // open_sound_channels will open as many sound channels as it can without
  109. // consuming too much CPU time, up to a maximum defined by MAX_SOUND_CHANNELS
  110. // (currently 8). I define "too much CPU time" as (65 - 5n)%, ie it will open
  111. // two sound channels if it can be done with 55% CPU, three in 50%, four in
  112. // 45%, five in 40%, etc. The intention is to ensure that slow machines get
  113. // a small number of channels which use a sensible amount of CPU time, and
  114. // faster machines get more sound channels. The reason for the sliding scale
  115. // is that setting a fixed limit of 20% would mean that slower Macs got no
  116. // sound channels at all, and a fixed limit of 60% would mean that a PowerMac
  117. // 8100 would end up opening about 57 sound channels.
  118. //
  119. void open_sound_channels(void)
  120.     {
  121.     short num_sound_channels = 0;
  122.     while (end_sound_channels < &sound_channel[MAX_SOUND_CHANNELS])
  123.         {
  124.         SndChannelPtr sp = &end_sound_channels->chan;
  125.         sp->qLength = stdQLength;
  126.         
  127.         // except for first channel, check CPU loading
  128.         if (end_sound_channels > &sound_channel[0])
  129.             {
  130.             SndCommand myCmd;
  131.             myCmd.cmd = totalLoadCmd;
  132.             myCmd.param1 = 0;
  133.             myCmd.param2 = initMono;
  134.             // stop if we would exceed CPU usage limit
  135.             // by allocating this sound channel
  136.             if (SndControl(sampledSynth, &myCmd)) break;
  137.             if (myCmd.param1 > 60 - num_sound_channels * 5) break;
  138.             if (FreeMem() < 0x5000L) break;       // if < 20K left, stop now.
  139.             }
  140.         if (SndNewChannel(&sp, sampledSynth, initMono, NULL)) break;
  141.         end_sound_channels++;
  142.         num_sound_channels++;
  143.         }
  144.     }
  145.  
  146. // **************************************************************************
  147. //
  148. // When the application wishes to stop making sounds, the sound channels
  149. // should be closed. This routine should be called in response to the
  150. // application being put in the background (i.e. a MultiFinder 'suspend'
  151. // event) or when the user selects a "Turn Off Sound Effects" menu item.
  152. // If you wish to have your program (game?) continue making sounds in the
  153. // background then call close_sound_channels with keep_some=TRUE and it
  154. // will keep a single sound channel open. Keeping more than one channel
  155. // open when in the background would probably not be in the spirit of
  156. // 'cooperative' multitasking.
  157. //
  158. void close_sound_channels(Boolean keep_some)
  159.     {
  160.     mySndChannel *stop = &sound_channel[0];
  161.     // If we want background sound, then leave one sound channel open
  162.     if (keep_some) stop = &sound_channel[1];
  163.     while (end_sound_channels > stop) 
  164.         SndDisposeChannel(&(--end_sound_channels)->chan, TRUE);
  165.     }
  166.  
  167. // **************************************************************************
  168. //
  169. // makesound is what the program calls when it wants to make a sound.
  170. // Handle s          is a handle to the 'snd ' resource to be played.
  171. // u_short priority  indicates the relative importance of different sounds.
  172. // SND_TYPE t        indicates what action to take
  173. //                   if the sound is already playing
  174. // u_char volume     is the volume (0-255) at which the sound is to be played
  175. //
  176. // The priorities allow some sounds to replace others. E.g. If a priority 2
  177. // sound is playing and a priority 1 sound happens, then if there are no
  178. // free sound channels left, the priority 2 will be interrupted to play the
  179. // priority 1 sound instead. If a priority 2 sound happens when all the
  180. // sound channels are busy playing priority 1 sounds, then the priority 2
  181. // sound will be ignored.
  182. //
  183. // Some sounds, like shots fired, are individual sounds in their own right.
  184. // Other sounds like running water, are intended to be a single continuous
  185. // sound. The SND_TYPE indicates what action to take if that sound is already
  186. // playing:
  187. // SND_INDIVIDUAL: Play a new independent sound
  188. // SND_REPLACE:    Halt the currently playing sound and restart it
  189. // SND_NOREPLACE:  Let the existing sound continue and ignore the new one
  190. //
  191. // The volume level is used in Bolo to make distant sounds quieter.
  192.  
  193. #define sound_age(X) (timenow - (X)->starttime)
  194. #define weighted_priority(X) ((X)->priority + (sound_age(X) << 2))
  195. #define MIN_SAFE_SOUND_AGE 8
  196.  
  197. #define my_chan_busy(X) ((X)->killsound || (X)->makesound || (X)->busy)
  198.  
  199. void makesound(Handle s, u_short priority, SND_TYPE t, u_char volume)
  200.     {
  201.     mySndChannel *i = end_sound_channels;   // Start with my "not found" value
  202.     mySndChannel *bestslot = &sound_channel[0];
  203.     long timenow = TickCount();
  204.     u_char interrupt = TRUE;
  205.     
  206.     if (!s || !*s) return;
  207.     
  208.     // For special sounds, see if sound is already playing, and if so,
  209.     // decide  whether to replace it or ignore it
  210.     if (t != SND_INDIVIDUAL)
  211.         for (i=&sound_channel[0]; i<end_sound_channels; i++) 
  212.             if (i->soundhandle == s && my_chan_busy(i))
  213.                 {
  214.                 if (t == SND_REPLACE && sound_age(i) >= MIN_SAFE_SOUND_AGE)
  215.                     break;
  216.                 else return;
  217.                 }
  218.  
  219.     // If not special sound, or special sound not found,
  220.     // try to find a free channel
  221.     if (i == end_sound_channels)
  222.         for (i=&sound_channel[0]; i<end_sound_channels; i++)
  223.             {
  224.             // If channel not busy, use it
  225.             if (!my_chan_busy(i)) { interrupt = FALSE; break; }
  226.             // make sure sound is old enough to safely interrupt it
  227.             if (sound_age(i) >= MIN_SAFE_SOUND_AGE &&
  228.                 weighted_priority(i) > weighted_priority(bestslot))
  229.                     bestslot = i;
  230.             }
  231.  
  232.     // If still haven't found a channel to use, see if we can steal another
  233.     if (i == end_sound_channels)
  234.         {
  235.         if (priority > weighted_priority(bestslot) ||
  236.             sound_age(bestslot) < MIN_SAFE_SOUND_AGE) return;
  237.         else i = bestslot;
  238.         }
  239.  
  240.     // OK, now fill in the details for the service routine to act on
  241.     i->soundhandle = s;
  242.     i->priority    = priority;
  243.     i->starttime   = timenow;
  244.     i->volume      = volume;
  245.     i->killsound   = interrupt;
  246.     i->makesound   = TRUE;
  247.     }
  248.  
  249. // **************************************************************************
  250. //
  251. // service_sound_channels must be called periodically, say each time around
  252. // your main event loop, or, if you want to be really bold, from a 60Hz VLB
  253. // task. The Sound Manager is not particularly stable when called from VBL
  254. // tasks or other interrupt routines. It won't crash the Mac, but it may
  255. // make garbage squawking sounds occasionally, so it may help to disable
  256. // interrupts as indicated below. 
  257. //
  258. void service_sound_channels(void)
  259.     {
  260.     mySndChannel *i;
  261.     // DISABLE_INTERRUPTS ?
  262.     for (i=&sound_channel[0]; i<end_sound_channels; i++)
  263.         {
  264.         SCStatus ss;
  265.         SndChannelStatus(&i->chan, sizeof(ss), &ss);
  266.         i->busy = (ss.scChannelBusy != 0);
  267.  
  268.         if (i->killsound)
  269.             {
  270.             SndCommand myCmd;
  271.             myCmd.cmd    = flushCmd;
  272.             myCmd.param1 = 0;
  273.             myCmd.param2 = 0;
  274.             SndDoImmediate(&i->chan, &myCmd);
  275.     
  276.             myCmd.cmd    = quietCmd;
  277.             myCmd.param1 = 0;
  278.             myCmd.param2 = 0;
  279.             SndDoImmediate(&i->chan, &myCmd);
  280.  
  281.             i->killsound = FALSE;
  282.             }
  283.         else if (i->makesound && !i->busy)
  284.             {
  285.             SndCommand myCmd;
  286.             myCmd.cmd    = ampCmd;
  287.             myCmd.param1 = i->volume;
  288.             myCmd.param2 = 0;
  289.             SndDoImmediate(&i->chan, &myCmd);
  290.         
  291.             myCmd.cmd    = bufferCmd;
  292.             myCmd.param1 = 0;
  293.             myCmd.param2 = (long)(*i->soundhandle) + 0x14;
  294.             SndDoImmediate(&i->chan, &myCmd);
  295.  
  296.             i->makesound = FALSE;
  297.             i->busy = TRUE;    // Yes, we have now made a sound
  298.             }
  299.         }
  300.     // RESTORE_INTERRUPTS ?
  301.     }
  302.