SoundApp.c

/*
    File:       SoundApp.c
 
    Contains:   1.04: This was an update to support the Utilities Unit that MacDTS
developed.  Some of it was routines formally created for SoundApp,
with additional ones coming from other sources.  This helps to reduce
the "destractive" code from the samples and makes them easier to maintain.
 
1.1:    ¥ This is the "new" SoundApp which adds some new features.
        ¥ Some knowledge of the new Sound Manager is present
          in areas that were work-arounds for old Sound Manager bugs
        ¥ Recording of sounds is now possible with the Sound Input Manager
        ¥ The document window will keep the Record button hidden if
          the Sound Inpute Manager is not available for recording,
          otherwise it will expand the window size to show this button.
        ¥ Supports copy and pasting of sound resources
        ¥ Conversion to MPW 3.2 was established (with some amount of pain)
        ¥ SoundApp now has its own documents
        ¥ Supports launching by open documents from Finder
        ¥ Added color icons for the System 7.0 desktop
        ¥ Added the new Sound Manager error strings
        ¥ DoErrorSound tests for safe beeps
        ¥ Made Apple Legal happy by getting rid of the xxxxCmd
          and changed it to the freqDurationCmd.  Also got rid of
          any other use of the work "xxxx" in a musical context
 
1.2:    ¥ Translated into C, some clean up along the way
        ¥ Using universal headers
        ¥ Gave up on the DTS utilities code, everything is self-contained
 
Formatting was done with FONT = Courier, SIZE = 10, TABS = 4
 
many thanks to: Bo3b Johnson, Mark Bennett, Andy Shebanow, Keith
Rollin, Chris Derossi, Pete Helme, Darin Adler, and my co-workers that
sat near me while I was making lots of noise testing this application.
 
To the reader,
 
SoundApp.p is a sample application source file for demonstrating
the Sound Manager.  It requires the use of the SoundUnit to handle
all of the sound routines.  This portion of the source code handles the
applicationÕs management of memory, errors, user interface, etc..
The read me notes are here in the source code because people tend to
not read them otherwise.
 
SoundApp is a demonstration of the Sound Manager released in System
6.0.x.  SoundApp is also a example of how to write a Macintosh
application that performs good user interface and proper error
handling.  Believe it or not, but the Sound Manager portion was the
easiest part for me to write.  It was all the user interface and error
handling that was the most difficult.  One thing became very clear to
me in the course of writing this application.  The following axiom is
one of the few great truths in the universe.
 
If you write a Macintosh application without MacApp, youÕre working
too hard.
 
Throughout the development of SoundApp I would run into typical
situations that make programming a Macintosh too hard.  When this
happened I asked myself Òwhat would MacApp do?Ó and that was followed
by the thought Òthen why am I writing code that is already out there
for me to use?Ó  I started out intending on writing a very simple
application that anyone could read, and understand the Sound Manager.
I felt this meant not requiring the person to read Object Oriented
Pascal.  I accomplished part of my goal.  People should be able to
learn how to use the Sound Manager (in its present condition), but it
didnÕt turn out to be such a simple application as I had hoped for.
 
I have put a large amount of comments into the code.  This is
something IÕm really picky about.  People do not comment their code
enough.  Each procedure has a paragraph that should explain what to
expect that routine to do, and how it goes about doing it.  There are
some bigger issues that I will put into the release notes below.
There are some things that make the Macintosh harder to develop for
than it should be.  Simple things should be simple.  Too many things
on the Mac that should be simple are not.  Maybe someday these things
will be fixed.
 
¥ GetFontInfo requires a port set to the font in question.  If I
wanted to find the height of the System Font, I had to first set the
current port to the WindowMgr.  I could have used my own window, but
what if I needed the font info before I had a window available?
¥ The toolbox blows chunks when your heap gets Òtoo low.Ó  I believe
this magical number is between 32k and zero.  The odds of blowing
chunks increase logarithmically as one approaches 0 free bytes.
¥ The Dialog Manager is not a free lunch and in fact will cost you
plenty.
¥ ThereÕs no safe way to determine how much memory opening a
resource file will take away from your heap.
¥ ThereÕs no way to determine how safe it is opening a resource that
could be shared by other applications, especially on a local volume.
¥ The Resource Manager doesnÕt always set ResErr.
¥ The Sound Manager returns even less errors.
¥ The List Manager returns no errors.
¥ Writing a staggering routine for new windows encompasses a number
of difficulties.  How does one find the height of a windowÕs title
region before the window is visible?
 
Am I just a complainer?  Do I have a bad attitude?  Probably, but
IÕm just trying to point out the above areas make the Mac programming
experience difficult.  These are areas that get developers into
trouble.  These areas need a sign in front of them that says,
ÒDanger!Ó  These are areas that developers get into and then write to
MacDTS for help.
 
Notation Conventions
--------------------
All global variables begin with a lower case ÒgÓ.
All constants begin with ÒkÓ except for those noted here.
Resource IDs begin with ÒrÓ.
Menu IDs being with ÒmÓ and items with an ÒiÓ.
Resource strings begin with ÒsÓ.
 
Human Interface
---------------
This is the most important and about the most difficult aspect of
programming on the Macintosh.  In the development of SoundApp I gave much
thought to the human interface issues.  In fact, in talking with the Human
Interface Group additions to the existing guidelines were made.  The
method of window stacking used here was a new one.  This was documented in
a Human Interface Tech Note.  I even made one compromise (hard to
believe!) suggested by the Human Interface Group.  I originally had the
buttons and the list in my choice of font and size.  They felt that
buttons should be in the System font and the list should also be the same.
I liked my font choice better, but the group had a point that I really
couldnÕt argue with.  That was, ÒIf there isnÕt a compelling reason to
change something standard, then donÕt change it.Ó  Buttons on a Macintosh
typically appear in the System font.  Changing the font, just because I
wanted to, was considered gratuitous.  Standard File is in the System font
and it also contains a list and buttons.  Since my window are very similar
to that dialog, IÕm using the System Font.
 
SoundApp is never modal unless an error occurs and I need to show and
alert.  Controls are inactivated for inactive windows.  The default button
is given the proper outline, and this outline disappears when the window
is deactivated.  Keyboard equivalents for the buttons cause the button to
appear as if it had been clicked in.  The check box in the Standard File
dialog remembers the userÕs last setting.  The about box is only
semi-modal.  It will allow the user access to switch to another application.
The status window under some circumstances was found to disappear too quickly,
so a built in delay was added.  Windows are centered or stacked according
to the Human Interface Guidelines.  The sound level isnÕt adjusted by the
application, and instead the users is informed of the current level and
told how it can be adjusted.
 
The About box
-------------
ItÕs rad.  Has a color icon, shows the 'vers' resource, goes Moof!ª.  It
also demonstrates how to handle a modal window without the Dialog Manager.
This technique can be use for any window, including dialog windows.  The
dialogÕs update routine would call UpdtDialog.  The really new point to
notice is this window is modal but ONLY within the applicationÕs layer.
While running under MultiFinder, the user can switch to other
applications.  While the About window is present, it is the only window
belonging to the application that accepts user actions.
 
Memory Management
----------------
This has to be the most difficult portion of a Mac application to
write.  This along with the user interface.  I spent too many nights
chasing down crashes while running the application under low memory.
I found unpleasant surprises while doing this.  The Sound Manager
doesnÕt check for NIL pointers.  OpenResFile may take large portions
of my heap away.  The toolbox seems to need at least 32k of free space
in the heap of my Mac II running color.
 
I wrote a simple grow zone procedure that will dump a reserve memory
block.  This is only considered for use in an emergency.  I never rely
on using it directly.  If the reserve has been released, I will not
continue an operation such as playing a sound or showing the status
window until it is regained.  Grow zones should not be considered a
solution to memory management.  They can be used to augment your
overall memory management scheme.
 
Error Checking
--------------
Lots and lots and lots of it.  I could even do more.  Programmers
need to do more of this.  The Sound Manager will crash when
encountering a NIL pointer.  My application should never crash.  If
you can find a way to crash this application, then I want to hear
about it.  Using a bogus 'snd ' resources doesnÕt count and IÕve found
many of those.  Writing proper error checking into the code during
development really helped.  Always handle errors, and pass along the
error.  I will let the first error encountered to be passed all the
way up to the caller and eventually my error dialog will show up for
the user.
 
SetPort Strategy
----------------
Any routine that needs to use Quickdraw will set the port.  I do not
believe that it should also be responsible for restoring the port back
to what it may have been before the routine was called.  So, youÕll
find there is an absence of the GetPort, SetPort, do my thing, and
then SetPort again.  Instead I SetPort and do my thing.  The Mac often
is setting the port unnecessarily.
 
Strings
-------
All of my strings are resources.  There are no strings that appear
within the code.  This helps memory management and allows me to adjust
the application to international systems without compiling any code.
I could simply use ResEdit, or some other tool, to localize this
application.  I find it is just as easy to run Rez again than
attempting to use ResEdit.  Besides, after editing with ResEdit I want
the source for that and would have to run DeRez which isnÕt nearly as
clean as my original source files.
 
Window Stacking
---------------
I hate applications that will open a new document that covers up an
existing document, unless the new document covers the entire screen.
So, my applicationÕs documents have a small window size.  I wanted to
open new windows that would not cover up older ones.  This is nice for
the user, since they will not have to move windows just to get at
other documents.  ResEdit will stagger new windows off of the
frontmost window but I find that this isnÕt the best approach.  It
will still cover up other windows and I also donÕt like windows that
will open half way between two monitors.  I wanted a better approach:
one that would always stagger new windows and not cover up older ones.
 
When I want to center a window, I need to know its entire rectangle
size.  The rub is that I cannot determine its size until I show it
because I only know about the windowÕs boundsRect.  This does not
include the area that contains the title bar.  ThatÕs the strucRgn,
which is an empty region for an invisible window.  I could do what
MacApp does, but if I have to do another thing that MacApp already
does IÕll give up and stick with MacApp.  I ended up writing a routine
that takes a guess at the height of the windowÕs title bar.  This is
another thing that was harder than it should have been.
 
Dialog Manager (and some of the reasons I donÕt like it)
--------------------------------------------------------
My first approach was to use modeless dialogs for document windows,
thinking that I could write an application that would demonstrate how to
deal with them and all of their idiosyncrasies.  Not long into the
development cycle it became obvious to me that I was fighting something
that contained more disadvantages than advantages.  I removed all the
dialog code and only used standard windows and controls the old fashion
way.  In the case of the About window, which is semi-modal, I have a test
that will return TRUE for a window that should be treated as modal.  This
allows my window to be handled by my standard event handlers and I donÕt
have to write dialog filters.  There are some things that do not get
handled properly while calling ModalDialog.  ModalDialog ignores disk
insert events.  The activate or update events do not get handled for
background windows.  Using a modeless dialog fails with MultiFinder if
switching takes place while the dialog is the frontmost window.  The
problem is that DialogSelect ignores and removes the suspend/resume event.
Another advantage to all this is that drawing was much faster.
 
As an example of some of the problems with ModalDialog and the activate
event.  Try this with the Finder.  Open a window and choose ÒView by name.Ó
Then select a few names with the shift key and resize the window so the
vertical scroll bar is visible.  Move this window to one edge of the
screen or a second monitor.  Now choose ÒSet Startup.Ó  This is a modal
dialog.  If you look at the Finder window with the selected files, youÕll
notice that the scrollbar and the text are still highlited.  This is not
the proper user interface.  This is because the deactivate routines are
not called while in ModalDialog.  You can even find this problem with
SoundApp.  On deactivate events I will change my controls to the inactive
state.  If you place the buttons to the side of the screen and then bring
up the standard file dialog, youÕll notice that the buttons donÕt change
properly.  ModalDialog also prevents the application from updating
background windows too.  To solve this a dialog filter procedure is
required.  In most cases, this filter would be as complex and the event
loop.  It would also make it necessary to call your event routines from
outside of the normal event loop.  All on this isnÕt worth the effort.
 
You can see how this does not happen while using this applicationÕs
About window.  Select an item in the document window and choose ÒPlay
Melody.Ó  This will leave the status window on screen so that you can
drag it to cover the document window.  Now select ÒAbout SoundAppÓ to
bring up the about window.  This causes the status window to close,
which uncovers the document window leaving an invalid area.  The
document window gets an activate event, then the About window appears.
Then the document window is properly deactivated and updated.  Yeah,
just like it should happen.
 
So, the tradeoff was that I didnÕt have to work around all the
strange things the Dialog Manager does such as running a secondary
event loop, and requiring me to have userItems or filterProcs.  This
made the code smaller, more readable, and faster.  I think I will
avoid the Dialog Manger from now one unless IÕm using a very simple
dialog.  The about window of this application proved too much for the
Dialog Manager.
 
One thing dialogs are good for is running ResEdit and laying out the
dialog.  To help position controls, I used a DLOG resource of the same
size as my WIND resource.  The DITL of this dialog contains the
positions I wanted for my CNTL resources.  This helped me to look at
where I could expect my buttons to show up.  This is one of the main
reasons people think they need the Dialog Manager, because ResEdit
makes it easy to build dialogs.  ResEdit alone has contributed to
nearly all of the Dialog Manager abuse in the world today.
 
I used a Rect resource for positioning the list rectangle of the
document windows.  These windows look very much like a modeless
dialog.  (They used to be, but that presented to many problems.)  The
About window is also a standard window, but shown modally.  Just like
ModalDialog, but my modal window does allow switching under
MultiFinder.  You can change the window to a dBoxProc and then
MultiFinder will not switch while this is the active window.  To help
with the layout of the about window, I position the text within it
based on the size of the window.  The status window does this too.
These two things, the Rect resource and text based on the size of the
window, help when changing the text.  If the new text doesnÕt fit,
then resize the windowÕs resource.  I used some trick with Rez to help
layout my window contents.  Refer to the SoundApp.r sources.
 
IÕve read and understood Tech Note 203, and have learned how to
apply it.  Bo3b Johnson is a smart guy, and developers should trust
his opinions.
 
List Manager
------------
ItÕs very easy to be tempted by this part of the toolbox, along with
the Dialog Manager.  The List Manager is a slow beast at times.  It
also has some problems with Òdoing the right thing.Ó  IÕve found that
the list will not be updated properly when the user clicks in a cell
that is out of bounds.  LClick will return TRUE with a cell that
doesnÕt exists.  LActivate will erase the scrollbars instead of
highlighting the properly.  Finally, the List Manager does not return
errors.  How would a person know if LSetCell worked?
 
IÕve read and understood Tech Note 203, and have learned how to
apply it.
 
Resource Manager
----------------
I test all the handles being returned from the Resource Manager
before using them, and if I get a NIL then I look at ResError.
ResError sometimes lies and returns noErr and a NIL handle.  ResError
is usually good for getting an error code AFTER youÕve already found
an error.
 
Opening a resource file that is already open by another application
is dangerous.  The Resource Manager will not tell you when youÕve done
this.  There needs to be a OpenRFPerm that will return permission
errors such as resFileBusyErr.  Refer to Tech Note 185.
 
When I or the Toolbox needs to get at one of my resources,
CurResFile must be set to my application.  Also, look out for one
particularly nasty situation when switching resource files.   If the
segment loader goes for a CODE segment, it better be from our resource
file!  The idea here is, in case you didnÕt get it already, always
have the current resource file be set to the application.  If a
resource is needed from another file, switch momentarily to get the
resource and immediately restore the current resource file to the
application.  I take an added measure of defense and whenever I need a
resource I use the Get1Resource calls.  These will only search the
current resource file.
 
Strategies For Sound
--------------------
All of the Sound Manager code is contained in the SoundUnit.p.  This
code was written to be general purpose, providing useful routines for
other applications.  Lots of error checking is performed.  IÕve also
extended the support for SndPlay and made it really asynchronous.
IÕve demonstrated most of the abilities the present Sound Manager has
to offer.  I will have to revise the SoundAppUnit to include any new
features (e.g., multi channel support) when the next Sound Manager is
released.
 
I allocate my own memory to be used as sound channels.  I allocate
these pointers early in the applicationÕs startup time to avoid memory
fragmentation.  These channels are of the standard size (holding 128
commands) but IÕve extended the structure to include my own
information.  When I create a new sound channel, I pass it a pointer
to this memory.  This will link in the 'snth' resource and hardware to
my channel.  When I dispose of the channel, the Sound Manager will
purge this resource and disconnect me from the hardware.  When adding
the 'snth' resource, the Sound Manager will allocate a pointer into
the applicationÕs heap instead of the systemÕs.  This is a modifier
stub used by the 'snth'.  This could cause some problems with memory
management.  I create and dispose of all my channels as soon as
possible, and this doesnÕt cause me problems.
 
I keep track of which document is playing a sound, along with a
global of when the application is playing sound.  I needed to keep
track of which document is playing because if the user disposes of
that document, I will have to stop playing the sound contained in it
since the user wants to dispose of that data.  I keep track of when
the application is playing sound in a global.  This is only used by
the routine that calculates the sleep time for WaitNextEvent.
 
I came up with a pretty sick music notational system using Rez.
Refer to the notes in the SoundAppSnds.r file.  If youÕve just
finished a meal, wait four hours before reading.
 
The SoundUnit handles all of the Sound Manager code entirely.  This
eliminates any and all references to the Sound Manager from the
application.  The SoundUnit will return any error encountered while
calling the Sound Manager, and does some extra error checking the
Sound Manager doesnÕt do.
 
The portion of the application that uses the wave table synthesizer
is more complex than the other two.  I wanted to include an example
channel modifier for use in the wave table channels.  This would have
been a transpositional modifier that would take a given freqDurationCmd and
transpose it by some amount.  This would be nice for the routine that
plays a scale, by allowing the other three channels to be playing the
same scale but at a different interval.  Unfortunately, I found that
the Sound Manager has bugs using a modifier, at least with the wave
table synths, and could not use them.
 
IÕve created a few wave table sounds and keep them in a 'snd '
resource.  This allows me to change the sound of the wave table
channels and not change any of the code.  Creating wave table data is
complicated.  The example sounds IÕve included are samples IÕve taken
from various sources.  IÕve cleaned them up quit a bit.  This was to
set loop points, try and reduce clicks, correct the sample rates, and
base frequencies.  This is also a complicated task.  Maybe I should document
these techniques.
 
    Written by: Jim Reekes  
 
    Copyright:  Copyright © 1994-1999 by Apple Computer, Inc., All Rights Reserved.
 
                You may incorporate this Apple sample source code into your program(s) without
                restriction. This Apple sample source code has been provided "AS IS" and the
                responsibility for its operation is yours. You are not permitted to redistribute
                this Apple sample source code as "Apple sample source code" after having made
                changes. If you're going to re-distribute the source, we require that you make
                it clear in the source that the code was descended from Apple sample source
                code, but that you've made changes.
 
    Change History (most recent first):
                7/29/1999   Karl Groethe    Updated for Metrowerks Codewarror Pro 2.1
                
 
*/
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// global defines
 
// All of this code is for System 7 and later
#define SystemSevenOrLater 1
 
// I don't use any obsolete stuff in the interfaces, and you shouldn't either.
// You also need to worry about names because the Code Fragment Manager binds
// system calls by name. Read the comments in ConditionalMacros.h
#define OLDROUTINENAMES 0
 
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// includes
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#include <FixMath.h>
#include <AppleEvents.h>
#include <Controls.h>
#include <Devices.h>
#include <DiskInit.h>
#include <Dialogs.h>
#include <Errors.h>
#include <Events.h>
#include <Files.h>
#include <Icons.h>
#include <Gestalt.h>
#include <Lists.h>
#include <LowMem.h>
#include <Menus.h>
#include <Memory.h>
#include <QuickDraw.h>
#include <Resources.h>
#include <Scrap.h>
#include <Script.h>
#include <SegLoad.h>
#include <StandardFile.h>
#include <TextUtils.h>
#include <ToolUtils.h>
#include <Traps.h>
#include <Types.h>
#include <Windows.h>
 
#include <limits.h>
#include <string.h>
 
#include <Sound.h>
#include <SoundInput.h>
#include "SoundUnit.h"
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// constants
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
enum {
    kOSTrapBit              = (1<<11),  /* bit 11 is the OS Trap bit */
    kTrapNumberMask         = 0x07FF,   /* bits used by the A-Trap mechanism */
 
    kPollingSleepTime       = 60,       //MultiFinderÕs sleep while sound is playing
 
    kNumberOfMasters        = 3,        //number of master pointer blocks we expect
    kSizeOfReserve          = (unsigned long)32 * 1024, //size of reserve memory for grow zone proc
    kMinSpace               = (unsigned long)32 * 1024, //minimum available memory I allow in heap
    kMemForSndDoc           = 20 * 1024, //minimal amount of memory needed by document
 
    rAppSignature           = 'SAPP',   //applicaitonÕs OS signature
    rSndAppDocType          = 'sDoc',   //document's file type
 
    kScrollbarAdjust        = 16-1,     //the width of the scrollbar in the list
    kListFrameInset         = -1,       //inset rectangle adjustment for list frame
    kStadardWhiteSpacing    = 13,       //inset rectangle adjustment for dialog items
 
    kRecordTop              = 40,       //40,50 is topLeft for SndRecord dialog
    kRecordLeft             = 50,       //40,50 is topLeft for SndRecord dialog
 
    kHiliteControlSelect    = 1,        //select the control
    kHiliteControlDeselect  = 0,        //deselect the control
    kCntlOn                 = 1,        //controlÕs value when truned on
    kCntlOff                = 0,        //controlÕs value when truned off
 
    kWindowPosStartPt       = 2,        //offset from the topLeft of the screen
    kWindowPosStaggerH      = 16,       //staggering amounts for new windows
    kWindowPosStaggerV      = 3,        //not including the windowÕs title height
 
    kMinWindowTitleHeight   = 19,
    kButtonFrameSize        = 3,        //buttonÕs frame pen size
    kButtonFrameInset       = -4,       //inset rectangle adjustment around button
    kButtonSizeH            = 17,       //standard height of buttons
    kDafaultButSizeH        = kButtonSizeH - kButtonFrameInset - kButtonFrameInset
};
 
//refer to the SoundApp.r file for the explaination about these numbers
enum {
    kNumOfButtons           = 6,        //the number of buttons in the window
    kSndButtonSizeW         = 100,      //SizeW of buttons in document window
    kSndButtonSizeH         = 22,       //heigth of buttons in document window
    kSoundWindowSizeH       = ((kNumOfButtons * (kStadardWhiteSpacing + kSndButtonSizeH))
                                + kStadardWhiteSpacing),
    kSoundWindowSizeW       = 296,
 
    //standard position for icons in alerts
    kIconWidthOrHeight      = 32,
    kStdAlertIconTop        = kStadardWhiteSpacing,
    kStdAlertIconLeft       = 23,
    kStdAlertIconBottom     = kStdAlertIconTop + kIconWidthOrHeight,
    kStdAlertIconRight      = kStdAlertIconLeft + kIconWidthOrHeight,
 
    kFSAsynch               = true,     //asynchronous File Manager call
 
    kEnterKey               = '\3',     //the keys IÕm looking for
#ifdef applec
    kReturnKey              = '\n',     //MPW C is wrong, the return key is "\r" not "\n"
#else
    kReturnKey              = '\r',
#endif
    kEscape                 = '\33',
    kUpArrow                = '\36',
    kDownArrow              = '\37',
    kPeriod                 = '.',
    kBackspace              = '\b',
 
//This bit set in the ioFlAttrib field if the fileÕs resource fork is open.
    kResForkOpenBit         = (1 << 2),
 
//For the delay time when flashing the menubar and highlighting a button.
    kDelayTime              = 8,        //8/60ths of a second
 
//The minimal number of ticks the ShowWindow needs to be visible.
    kShowTimeDelay          = 20,
 
//BUG NOTE: timbreCmd value of 255 on the Mac Plus/SE will cause a crash Sound Manager 1
    kSquareWave             = 240,      //square wave for squareWaveSynth
    kSineWave               = 0,        //sine wave for the squareWaveSynth
    kPreferredTimbre        = 190,      //my preferred timbre for the squareWaveSynth
 
//Application snd resources must be higher than this number.
    kSystemSndRange         = 8191      //This is the highest snd id reserved by Apple.
};
 
//The following constants are the resource IDs.
enum {
    rMenuBar                = 1000,     //applicationÕs menu bar
 
    rExitAlert              = 1000,     //emergency exit user alert
    rUserAlert              = 1001,     //error message user alert
    rSoundVolAlert          = 1002,     //sound is set low alert
    rSaveAlert              = 1003,     //save changes? dialog
 
    rGetNameDLOG            = 2000,     //get a name for the sound dialog
    rNameItem               = 3,        //edit text item in rGetNameDLOG
    rUserItem               = 5,        //user item to help draw default outline
 
    rCustomGetFileDLOG      = 2001,     //dialog template for CustomGetFile
    rSndOnlyCheckBox        = 10        //dialog item number in CustomGetFile
};
 
/*
These are the window IDs used in the applications.  Each one must be unique,
since they are used to identify which window the event took place in.
*/
enum{
    rAboutWindow            = 1000,     //about window
    rStatusWindow           = 1001,     //sound status window
    rSoundWindow            = 1002,     //sound document window
 
    rListRectID             = 1000,     //resource containing size of list rectangle
 
    rCancelCntl             = 1000,     //stop button ID for the status window
    rPlaySndCntl            = 1001,     //sound button IDs for the sound document window
    rHyperPlayCntl          = 1002,
    rPlayScaleCntl          = 1003,
    rMelodyCntl             = 1004,
    rStopCntl               = 1005,
    rRecordCntl             = 1006,
    rAboutOkCntl            = 1007,
    rUntitled               = 1000,     //string ID for untitled resources
    rAboutText              = 1001,     //string ID for text appearing in about window
    rPutFileMsg             = 1002,     //string for text appearing in SFPutFile dialog
    rMoofIcon               = 1000,     //resource ID for ICON of application
    rAppPict                = 1000,     //resource ID of picture shown in about window
    rSndCursor              = 1000      //cursor resource for our documents
};
 
/*
The following are the snd resource IDs contained in the application,
which start at ID 9000.  IDs 0 - 8191 are reserved for Apple.
*/
enum {
    rMoofSound              = 9000,     //snd for the about window
    rScaleSnd               = 9001,     //snd containing a scale
    rMelodyPart1            = 9002,     //snd containing a melody
    rMelodyPart2            = 9003,     //snd containing the harmony
    rMelodyPart3            = 9004,     //snd containing the harmony
    rMelodyPart4            = 9005,     //snd containing the harmony
    rWaveHarmony            = 9006,     //snd containing waveTable for harmony
    rWaveMelody             = 9007,     //snd containing waveTable for melody
    rCounterPt1             = 9008,     //snd containing soprano part of counter point
    rCounterPt2             = 9009,     //snd containing alto part of counter point
    rCounterPt3             = 9010,     //snd containing tenor part of counter point
    rCounterPt4             = 9011,     //snd containing bass part of counter point
    rSopranoVox             = 9012,     //snd containing waveTable of soprano voice
    rAltoVox                = 9013,     //snd containing waveTable of alto voice
    rTenorVox               = 9014,     //snd containing waveTable of tenor voice
    rBassVox                = 9015,     //snd containing waveTable of bass voice
 
    rSampleHarmony          = 9016,     //snd containing the sampled sound harmony
    rSampleMelody           = 9017      //snd containing the sampled sound melody
};
 
//The following are resource IDs for messages.
enum {
    sErrStrings             = 1000,     //error string STR# ID
    sStandardErr            = 1,        //An error has occurred.
    sMemErr                 = 2,        //A Memory Manager error has occurred.
    sResErr                 = 3,        //A Resource Manager error has occurred.
    sCurInUseErr            = 4,        //That file is currently in use.
    sWavesBroken            = 5,        //The wave table synthesizer is not available.
    sWrongVersion           = 6,        //This system does not support the Sound Manager...
    sLowMemory              = 7,        //Memory is too low to continue...
    sNoMenus                = 8,        //Could not find applicationÕs menu resources.
    sInitSoundErr           = 9,        //Could not initialize the SoundUnit.
    sSoundErr               = 10,       //The Sound Manager has encountered an error.
    sNewDocErr              = 11,       //Could not create a new document.
    sInitStatusErr          = 12,       //Error initializing the status window.
    sEditErr                = 13,       //Could not complete the edit command.
    sDocErr                 = 14,       //There is a problem with this document.
 
    sMsgStrings             = 1001,     //message string STR# ID
    sPlayingMsg             = 1,        //playing a sound
    sHyperMsg               = 2,        //playing a sound the Hyper way
    sScaleMsg               = 3,        //playing scale
    sMelodyMsg              = 4,        //playing melody
    sTimbresMsg             = 5,        //playing various timbres
    sCounterPtMsg           = 6,        //playing 4 part counter point
 
    sSMErrStrings           = 1002      //strings describing Sound Manager errors
};
 
/*
The following constants are used to identify menus and items. The menu IDs
have an ÒmÓ prefix and the item numbers within each menu have an ÒiÓ prefix.
*/
 
enum {
    mApple                  = 128,      //Apple menu and items
    iAbout                  = 1,
 
    mFile                   = 129,      //File menu and items
    iNew                    = 1,
    iOpen                   = 2,
    iClose                  = 4,
    iQuit                   = 12,
 
    mEdit                   = 130,      //Edit menu and items
    iUndo                   = 1,
    iCut                    = 3,
    iCopy                   = 4,
    iPaste                  = 5,
    iClear                  = 6,
 
    mDemos                  = 1000,     //Demos menu and items
    iCheckVolume            = 1,
    iSquareScale            = 3,
    iSquareMelody           = 4,
    iSquareTimbre           = 5,
    iWaveScale              = 7,
    iWaveMelody             = 8,
    iWaveSATB               = 9,
    iSampleMelody           = 11,
    iSampleSATB             = 12
};
 
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// macros
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
// Define HighWord and LowWord macros for efficiency.
#define HighWord(aLong)     ((aLong) >> 16)
#define LowWord(aLong)      ((aLong) & 0x0000FFFF)
 
// accessing a rectangle's points
#define TopLeft(r)          (* (Point *) &(r).top)
#define BottomRight(r)      (* (Point *) &(r).bottom)
 
#define AbsoluteValue(n)    ((n > 0) ? n : -n)
 
#if GENERATINGCFM
#define CreateRoutineDescriptor(info, proc)                                 \
 RoutineDescriptor g##proc##RD = BUILD_ROUTINE_DESCRIPTOR(info, proc)
 
#define GetRoutineAddress(proc) (&g##proc##RD)
 
#else
#define GetRoutineAddress(proc) proc
#endif
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// types
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
typedef RectPtr *RectHandle;
 
/*
This is a document layout that contains the window record and references
to the associating data.  The window record MUST be the first field.  This
is because I use the window pointer returned by the Toolbox to be a
pointer to my document.  To confirm that the window pointer is a document
pointer, I store an application reference in the window recordÕs refCon.
Then, I use a routine to test for the presence of this reference to insure
IÕm looking at one of my documentÕs windows.
*/
 
struct SndDocument {
    WindowRecord    window;             // must be first field
    short           resFile;            // documentÕs resource file
    short           vRefNum;
    long            dirID;
    ListHandle      list;               // documentÕs list of sounds
    Boolean         sndInUse;           // document is using a 'snd ' resource
};
typedef struct SndDocument SndDocument;
typedef SndDocument *SndDocPeek;        // to peek at the document record
 
// This is the status window layout.  The concept here is similar to the
// document type mentioned above.  The message is a string handle used to
// store the current message.
 
struct StatusWindow {
    WindowRecord    window;
    StringHandle    message;            // current text of status message
    long            showTime;           // time window was shown
};
typedef struct StatusWindow StatusWindow;
typedef StatusWindow *StatWindowPeek;
 
// This is the about window layout.  The concept here is similar to the
// document type mentioned above.  The comment is a string handle used to
// store the current message.
 
struct AboutWindow {
    WindowRecord    window;
    Handle          appPict;            // handle to picture of appÕs name
    Handle          comment;            // handle to string of about comments
};
typedef struct AboutWindow AboutWindow;
typedef AboutWindow *AboutWPeek;
 
// This is the template to the WIND resource.  I used it to load in the WIND
// resource and then adjust the boundsRect.  I also look at the procID to
// determine if it has a title bar or drag region.
 
struct WindowTemplate {                 // template to a WIND resource
    Rect        boundsRect;
    short       procID;
    Boolean     visible;
    Boolean     filler1;
    Boolean     goAwayFlag;
    Boolean     filler2;
    long        refCon;
    Str255      title;
};
typedef struct WindowTemplate WindowTemplate;
typedef WindowTemplate *WindowTPtr, **WindowTHndl;
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// inlines
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
/*
This is a handy routine to copy pascal strings. It's smaller than the
library call pstrcopy, and faster too.
*/
 
#if GENERATING68K
#pragma parameter PStringCopy(__A0,__A1)
void PStringCopy(StringPtr source, StringPtr destination)
 FIVEWORDINLINE (0x7000, 0x1010, 0x12D8, 0x51C8, 0xFFFC);
//                  moveq   #0,d0
//                  move.b  (a0),d0
//          loop    move.b  (a0)+,(a1)+
//                  dbra    d0,loop
#else
#include <DriverServices.h>
#define PStringCopy(source, dest) PStrCopy(dest, source)
#endif
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// prototypes
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
short NumToolboxTraps(void);
Boolean TrapExists(short theTrap);
pascal long MyGrowZone(Size cbNeeded);
Boolean LowOnReserve(void);
void RecoverReserve(void);
Boolean AllocateReserve(void);
Boolean FailLowMemory(long memRequest);
Boolean HasSelection(SndDocPeek sndDoc);
OSErr GetSelection(SndDocPeek sndDoc, SndListHandle *sndHandle);
void SelectNextCell(ListHandle list, Boolean next);
void SelectSndCell(SndDocPeek sndDoc, short resID);
OSErr InitSndList(SndDocPeek docPtr);
Boolean OpenByApp(FSSpecPtr file, WindowPtr *window);
Point GetGlobalMouse(void);
short GetWTitleHeight(short variant);
Point GetGlobalTopLeft(WindowPtr window);
void MyParamText(StringHandle text, StringPtr cite0, StringPtr cite1);
void CenterWindowRect(short variant, Rect *theRect);
WindowPtr GetCenteredWindow(short id, Ptr p, WindowPtr behind);
short CenteredAlert(short alertID);
DialogPtr GetCenteredDialog(short id, Ptr p, WindowPtr behind);
void DoButtonOutline(ControlHandle button);
void SelectButton(ControlHandle button);
void ActivateSndCntls(SndDocPeek sndDoc);
Boolean IsDAWindow(WindowPtr window);
Boolean IsDocWindow(WindowPtr window);
Boolean IsModalWindow(WindowPtr window);
void KillSound(void);
Boolean DoCloseWindow(WindowPtr window);
void AlertUser(short error, short messageID);
void EmergencyExit(short message);
void Terminate(void);
void DrawStatusWindow(void);
void ShowStatusWindow(short messageID);
void InitStatusWindow(void);
pascal void DoErrorSound(short soundNo);
void CheckSoundVolume(void);
void AddSndDocControls(WindowPtr window);
Boolean PositionAvailable(Point newPt, short wTitleHeight);
WindowPtr NewStackedWindow(short windID, Ptr windStorage);
OSErr CreateSoundDoc(short resRef, FSSpecPtr file);
void OpenSoundDoc(FSSpecPtr file);
void NewSoundDoc(void);
pascal Boolean SFFilter(CInfoPBPtr p, Boolean *sndFilesOnly);
pascal short SFGetHook(short mySFItem, DialogPtr dialog, void *sndFilesOnly);
void GetSoundDoc(void);
void DrawAboutWindow(WindowPtr window);
void DoAbout(void);
void PlaySelectedSnd(SndDocPeek sndDoc, short message);
void PlaySndSong(SndDocPeek sndDoc, short sndID);
void PlaySquareSong(short sndID);
void PlaySquareTimbres(void);
void PlayWaveScale(void);
OSErr GetSndSongs(short sndSongID1, short sndSongID2, short sndSongID3, short sndSongID4, SndListHandle *sndSong1, SndListHandle *sndSong2, SndListHandle *sndSong3, SndListHandle *sndSong4);
OSErr InstallWaveSnds(SndChannelPtr chan1, SndChannelPtr chan2, SndChannelPtr chan3, SndChannelPtr chan4, short waveID1, short waveID2, short waveID3, short waveID4);
void PlayWaveMelody(void);
void PlayWaveSATB(void);
void PlaySampleMelody(void);
void PlaySampleSATB(void);
pascal void DefaultOutline(WindowPtr window, short theItem);
void GetSndName(Str255 sndName);
OSErr AddSnd(SndDocPeek sndDoc, StringPtr sndNamePtr, SndListHandle sndHndl);
void ClearSnd(SndDocPeek sndDoc);
void DoRecordSound(SndDocPeek sndDoc);
void AdjustMenus(void);
void CopySnd(SndDocPeek sndDoc);
void CutSnd(SndDocPeek sndDoc);
void PasteSnd(SndDocPeek sndDoc);
void DoMenuCommand(long menuResult);
void DrawSndWindow(WindowPtr window);
void DoKeyDown(char key, WindowPtr window);
Boolean ListClick(SndDocPeek sndDoc, EventRecord *event);
void DoSndDocClick(SndDocPeek sndDoc, EventRecord *event);
void DoStatClick(StatWindowPeek statWindow, EventRecord *event);
void DoAboutClick(WindowPtr window, EventRecord *event);
void DoUpdate(WindowPtr window);
void DoActivate(WindowPtr window, Boolean becomingActive);
pascal OSErr QuitApplicationEvent(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon);
pascal OSErr OpenDocumentsEvent(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon);
pascal OSErr PrintDocumentsEvent(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon);
pascal OSErr OpenApplicationEvent(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon);
void DoEvent(EventRecord *event);
void AdjustCursor(RgnHandle region);
void EventLoop(void);
void Initialize(void);
void main(void);
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Globals (The ÒgÓ prefix is used to emphasize that a variable is global.)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
/*
gMac is used to hold the result of a SysEnvirons call. This makes it
convenient for any routine to check the environment. It is considered
global information, anyway.
*/
 
    SysEnvRec gMac;                 //set up by Initialize
 
/*
gReserveMemory is a handle of reserve memory used by my grow zone
procedure.  If memory is attempted to be allocated and fails, my grow
zone will release this reserved block of memory.
*/
 
    Handle gReserveMemory;
 
/*
gStatusWindow is the window that tells the user whatÕs happening.  I show
it while a sound is playing, and remove it as soon as the sound is
complete.  It is initialized once during startup, and from then on two
routines are used to show and hide it.  This demonstrates orchestrating
multimedia, that sound/graphic idea, at least in a crude sort of way.
*/
 
    StatWindowPeek gStatusWindow;
 
/*
gAppResRef is the applicationÕs resource file reference.  I need to save
this since I can open other resource files.  The current resource file is
always gAppResRef unless I momentarily set it to another file to read its
resources, and then immediately restore it back.
*/
 
    short gAppResRef;               //set up by Initialize
 
/*
gInBackground is maintained by our osEvent handling routines. Any part of
the program can check it to find out if it is currently in the background.
*/
 
    Boolean gInBackground;          //maintained by Initialize and DoEvent
 
/*
We have to allocate our own QuickDraw globals when creating code for PowerMacs.
MetroWerks declares "qd" in their runtime, so don't do that twice.
*/
 
#if GENERATINGCFM && !defined(__MWERKS__)
    QDGlobals   qd;
#endif
 
/*
This is necessary because of the Apple Event Manager. We used to be able
to quit directly when the user choose Quit from the File menu. But because
of the design of Apple Events, we cannot call ExitToShell from the quit handler.
That event handler must set a flag and then check it in the event loop.
*/
 
    Boolean gTerminate;
 
 
// allocate the RoutineDescriptors for Power Mac toolbox calls
#if GENERATINGCFM
CreateRoutineDescriptor(uppGrowZoneProcInfo, MyGrowZone);
CreateRoutineDescriptor(uppFileFilterYDProcInfo, SFFilter);
CreateRoutineDescriptor(uppDlgHookYDProcInfo, SFGetHook);
CreateRoutineDescriptor(uppUserItemProcInfo, DefaultOutline);
CreateRoutineDescriptor(uppUserItemProcInfo, QuitApplicationEvent);
CreateRoutineDescriptor(uppUserItemProcInfo, OpenDocumentsEvent);
CreateRoutineDescriptor(uppUserItemProcInfo, PrintDocumentsEvent);
CreateRoutineDescriptor(uppUserItemProcInfo, OpenApplicationEvent);
#endif
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// IMPLEMENTATION
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
InitGraf is always implemented (trap 0xA86E). If the trap table is big
enough, trap 0xAA6E will always point to either Unimplemented or some other
trap, but will never be the same as InitGraf. Thus, you can check the size
of the trap table by asking if the address of trap 0xA86E is the same as 0xAA6E.
*/
 
#pragma segment Initialize
short NumToolboxTraps(void)
{
    if ( GetToolboxTrapAddress(_InitGraf) == GetToolboxTrapAddress(_InitGraf + 0x0200) )
        return (0x0200);
    else
        return (0x0400);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Check to see if a given trap is implemented.  GetTrapAddress may screw up and
wrap around into the trap table if you give it a toolbox trap that is out of
range, so check for this first.
*/
 
#pragma segment Initialize
Boolean TrapExists(short theTrap)
{
    UniversalProcPtr    trapAddress;
 
    if ( !(theTrap & kOSTrapBit) )                      // is this an OS trap?
        trapAddress = GetOSTrapAddress(theTrap);
    else {                                              // no, this is a tool trap
        theTrap &= kTrapNumberMask;                     // get the trap number
        if ( theTrap < NumToolboxTraps() )              // can this tool trap exist?
            trapAddress = GetToolTrapAddress(theTrap);
        else
            return (false);
    }
    return ( GetToolboxTrapAddress(_Unimplemented) != trapAddress );
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This is a very basic grow zone procedure.  My application keeps a reserve
handle of memory in case the Memory Manager gets a request for some memory
that is not available in my heap.  If memory were to get tight (<32k),
the Toolbox will crash the system, especially Quickdraw.  Before releasing
the reserve handle I make sure it isnÕt the GZSaveHnd.  This handle cannot
be touched by the grow zone procedure.
 
WARNING:  The grow zone procedure will be called and A5 may not be valid.
Read Tech Note #136 and 208
*/
 
#pragma segment Main
pascal long MyGrowZone(Size cbNeeded)
{
#pragma unused (cbNeeded)
    long theA5;
    long result;
 
    theA5= SetCurrentA5();
    if (((*gReserveMemory) != nil) && (gReserveMemory != GZSaveHnd())) {
        EmptyHandle(gReserveMemory);
        result = kSizeOfReserve;                    //released this much memory
    } else
        result = 0;                             //this may release more memory
    theA5= SetA5(theA5);
    return(result);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Before my application attempts to use more memory, I call this routine
to check if IÕm already using my reserve memory.  If so, then I better
prepare to die or get my reserve back.
*/
 
#pragma segment Main
Boolean LowOnReserve(void)
{
    return((*gReserveMemory) == nil);       // empty handle is low reserve
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This is called from the event loop if LowOnReserve returns that IÕm out of
the reserve memory.  This will recover the reserve memory block.  If this
fails, it will be called the next time through the event loop.
*/
 
#pragma segment Main
void RecoverReserve(void)
{
    ReallocateHandle(gReserveMemory, kSizeOfReserve);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This is called at startup time to allocate the reserve memory block used
in the grow zone procedure.  If IÕm unable to obtain this reserve, then
return false to signal a failure.
*/
 
#pragma segment Initialize
Boolean AllocateReserve(void)
{
    gReserveMemory = NewHandle(kSizeOfReserve);
    return(gReserveMemory != nil);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Call PurgeSpace and see if the requested amount of memory exists in the
heap including a minimal amount of heap space.  Also, if my grow zoneÕs
reserve has been release, thatÕs considered a failure.  I donÕt perform
any purging here.  The Memory Manager will do this if it needs the space.
This routine can be called with a memRequest == 0.  This checks if the heap
space is getting critical.
*/
 
#pragma segment Main
Boolean FailLowMemory(long memRequest)
{
    long    total;
    long    contig;
 
    PurgeSpace(&total, &contig);
    return((total < (memRequest + kMinSpace)) || LowOnReserve());
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This a simple test to see if the user has a currently selected list item.
*/
 
#pragma segment Main
Boolean HasSelection(SndDocPeek sndDoc)
{
    Cell aCell;
 
    SetPt(&aCell, 0, 0);
    return(LGetSelect(true, &aCell, sndDoc->list));
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Given a document, this will find the currently selected cell in the list.
Then using this cell as an index number, I call Get1IndResource.  This
will return the proper handle.  Since the List Manager is zero based and
the Resource Manager isnÕt, I have to add one to the index.
 
BUG NOTE: GetIndResource will return a bogus handle if the index is not
positive.
*/
 
#pragma segment Main
OSErr GetSelection(SndDocPeek sndDoc, SndListHandle *sndHandle)
{
    Cell    aCell;
    OSErr   result;
 
    result = noErr;
    SetPt(&aCell, 0, 0);
    if (LGetSelect(true, &aCell, sndDoc->list)) {
        if (aCell.v > -1) {                     // GetIndResource doesnÕt like < 0
            UseResFile(sndDoc->resFile);        // only get our resources
            *sndHandle = (SndListHandle)Get1IndResource(soundListRsrc, aCell.v + 1);
            if (*sndHandle == nil)
                result = ResError();            // return any error
            UseResFile(gAppResRef);             // restore our resource file
        }
    }
    return(result);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This is used to allow the user to select items from the list by using
the arrow keys.  Given a list and the direction, this will select the
next cell.  It will do nothing if weÕre at the first cell and the user
wants to do even higher (sped), or if weÕre at the last cell and the
user wants to go even lower (rent a clue bud).
*/
 
#pragma segment Main
void SelectNextCell(ListHandle list, Boolean next)
{
    Cell    aCell;
    short   lastItem;
 
    lastItem = (**list).dataBounds.bottom - 1;      // bounds is 1 greater
    SetPt(&aCell, 0, 0);
    if (LGetSelect(true, &aCell, list)) {
        if ((next && (aCell.v < lastItem)) || ((!next) && (aCell.v > 0))) {
            LSetSelect(false, aCell, list);
            if (next)
                aCell.v = aCell.v + 1;
            else
                aCell.v = aCell.v - 1;
            SetPt(&aCell, aCell.h, aCell.v);
            LSetSelect(true, aCell, list);
            LAutoScroll(list);
        }
    }
    else                                            // if no cells selected...
        LSetSelect(true, aCell, list);              // select the first cell
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
VERSION 1.1:  This routine will go through the list of snd resources
looking for the one that has the resource ID matching the parameter passed
in.  First thing to do is deselect the current selection.  Then once the
matching resource is found, select that one and scroll it into view.  One
assumption made is that the resources and the list are both index in the
same order.  This is true for the sndDoc, since I built the list this way.
*/
 
#pragma segment Main
void SelectSndCell(SndDocPeek sndDoc, short resID)
{
    Str255 name;
    Cell aCell;
    Handle sndHndle;
    ResType rType;
    short testID;
    short index;
    short numSnd;
 
    index = 0;
    aCell.h = 0;
    aCell.v = 0;
    if (LGetSelect(true, &aCell, sndDoc->list))
        LSetSelect(false, aCell, sndDoc->list); //deselect any cell
    UseResFile(sndDoc->resFile);                //count only its resources
    numSnd = Count1Resources(soundListRsrc);    //number of sounds available
    do {
        index++;
        SetResLoad(false);                      //donÕt load any resources
        sndHndle = Get1IndResource(soundListRsrc, index); //only get snd from file
        SetResLoad(true);                           //back to normal resource operations
        GetResInfo(sndHndle, &testID, &rType, name);
    } while ((resID != testID) && (index < numSnd));
 
    UseResFile(gAppResRef);                     //restore our resource file
    aCell.h = 0;
    aCell.v = index - 1;
    LSetSelect(true, aCell, sndDoc->list);
    LAutoScroll(sndDoc->list);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This is used to create the list showing all the snd resources in the file.
If anything goes wrong while attempting to get any of the resources, then I
return false.  I donÕt want the document to continue if something goes
wrong while reading a resource.  If I do get the resource, then I add its
name to the list in a new row.  IÕm assured that at least one resource is
in the file before this routine is called.  Untitled resources will get one.
*/
 
#pragma segment Open
OSErr InitSndList(SndDocPeek docPtr)
{
    short           index;
    short           resID;
    short           newRow;
    short           numSnd;
    Handle          resHandle;
    StringHandle    strHandle;
    ResType         itsType;
    Str255          resName;
    Str255          untitled;
    Cell            aCell;
    OSErr           result = noErr;
 
    strHandle = (StringHandle)Get1Resource('STR ', rUntitled);
    if (strHandle != nil)
        PStringCopy(*strHandle, untitled);      // save no name title
    else
        untitled[0] = 0;                        // at least an empty string
    UseResFile(docPtr->resFile);
    numSnd = Count1Resources(soundListRsrc);
    newRow = LAddRow(numSnd, 0, docPtr->list);
    for (index = 1; index <= numSnd; index++) {
        SetResLoad(false);                      // donÕt load any resources
        resHandle = Get1IndResource(soundListRsrc, index); // only get snd from file
        SetResLoad(true);                       // back to normal operations
        if (resHandle != nil) {                 // only if I got the snd
            GetResInfo(resHandle, &resID, &itsType, resName);
            if (StrLength(resName) == 0)        // if the snd isnÕt named...
                PStringCopy(untitled, resName); // give it a name
            SetPt(&aCell, 0, index - 1);
            LSetCell(&resName[1], StrLength(resName), aCell, docPtr->list);
        }
        else {                                  // resHandle == NIL
            result = resNotFound;               // problem with resource file
            break;                              // get out of the loop
        }
    }
    UseResFile(gAppResRef);                     // restore our resource file
    return(result);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
In the case that the user has attempted to open a resource file that is
already open, this routine will scan my open documents for it.  If I find
that the requested file is already open by the application, then return
true and its window pointer.  The application can then simply select that
window.  If the resource file is open but not by this application, thatÕs
a problem and I donÕt use the resource file.  Many problems with using an
open resource file.  Now if the Resource Manager would take care of this
problem for us...
*/
 
#pragma segment Open
Boolean OpenByApp(FSSpecPtr file, WindowPtr *window)
{
    Str255 docName;
    WindowPtr testWindow;
    Boolean result;
 
    result = false;
    testWindow = FrontWindow();
    while (testWindow != nil) {
        if (GetWRefCon(testWindow) == rSoundWindow)
            if ((file->vRefNum == ((SndDocPeek)testWindow)->vRefNum)
                && (file->parID == ((SndDocPeek)testWindow)->dirID))
            {
                GetWTitle(testWindow, docName);
                if (EqualString(file->name, docName, false, true))
                {
                    *window = testWindow;
                    result = true;
                    break;
                }
            }
        testWindow = (WindowPtr)(((WindowPeek)testWindow)->nextWindow);
    }
    return(result);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Get the global coordinates of the mouse.  Get the global coordinates by
calling GetMouse and LocalToGlobal.  This assumes the current port is a
valid graf port.  When wouldnÕt it be?
*/
 
#pragma segment Main
Point GetGlobalMouse(void)
{
    Point globalPt;
 
    GetMouse(&globalPt);
    LocalToGlobal(&globalPt);
    return(globalPt);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Try and determine the windowÕs title bar height.  This isnÕt easy,
especially if the window is invisible.  We all know how the standard Apple
System WDEF works, at least at this date we do.  I assume that method as
shown below.  I could do what MacApp does with the Function CallWDefProc.
If the user has installed a replacement for the System WDEF, then itÕs
their problem to deal with.  My method will work as long as Apple doesnÕt
change how the WDEF in System 6.0 calculates the title bar height.  This
will allow my application to work on international Macs that have a larger
system font than Chicago.  I check the windowÕs variant code for one that
includes a title bar.  I will have to change this routine to adjust for
the modal-moveable window type, which hasnÕt been defined yet.
 
In this routine, I violate my rule about not using the GetPort, SetPort,
SetPort sequence mentioned at the start of the file. Mostly, I do this
because itÕs not all that apparent that a routine called GetWTitleHeight
will change the port, so I make sure that it doesnÕt.
*/
 
#pragma segment Open
short GetWTitleHeight(short variant)
{
    FontInfo info;
    GrafPtr curGraf;
    GrafPtr wMgrPort;
    short wTitleHeight;
    short result;
 
    if (    (variant == documentProc)
         || (variant == noGrowDocProc)
         || (variant == zoomDocProc)
         || (variant == rDocProc) )
    {
        GetPort(&curGraf);
        GetWMgrPort(&wMgrPort);                     //I need to know the font...
        SetPort(wMgrPort);                          //info in the SystemÕs port
        GetFontInfo(&info);
        SetPort(curGraf);                           //restore current port
        wTitleHeight = info.ascent + info.descent + info.leading + 2;
        if (wTitleHeight < kMinWindowTitleHeight)
            wTitleHeight = kMinWindowTitleHeight;
        result = wTitleHeight;
    } else
        result = 0;                         //other window types have no title
    return(result);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Given a window, this will return the top left point of the windowÕs
port in global coordinates.  Something this doesnÕt include, is the
windowÕs drag region (or title bar).  This returns the top left point
of the windowÕs content area only.
 
In this routine, I violate my rule about not using the GetPort, SetPort,
SetPort sequence mentioned at the start of the file. Mostly, I do this
because itÕs not all that apparent that a routine called GetGlobalTopLeft
will change the port, so I make sure that it doesnÕt.
*/
 
#pragma segment Main
Point GetGlobalTopLeft(WindowPtr window)
{
    GrafPtr theGraf;
    Point globalPt;
 
    GetPort(&theGraf);
    SetPort(window);
    globalPt = TopLeft(window->portRect);
    LocalToGlobal(&globalPt);
    SetPort(theGraf);
    return(globalPt);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
IÕm using string pointers (not necessarily Memory Manager pointers)
to the replacement strings.  Using Str255 would copy the string to the
stack, and then to the text handle.  This way it is only copied once.
*/
 
#pragma segment Main
void MyParamText(StringHandle text, StringPtr cite0, StringPtr cite1)
{
    long    offSet;
    char    param0[2] = {'^', '0'};
    char    param1[2] = {'^', '1'};
    long    newLength;
 
    if (StrLength(cite0) > 0)
        offSet = Munger((Handle)text, 1, param0, sizeof(param0), (Ptr)&(cite0[1]), StrLength(cite0));
    if (StrLength(cite1) > 0)
        offSet = Munger((Handle)text, 1, param1, sizeof(param1), (Ptr)&(cite1[1]), StrLength(cite1));
    newLength = GetHandleSize((Handle)text) - 1;
    if (newLength > 255)
        newLength = 255;
    (*text)[0] = newLength;                     // stringÕs new length byte
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Given a windowÕs portRect, this routine will center the rectangle on the
main device within the desktop region.  This excludes the menu bar area.
It follows the Apple Human Interface Guidelines for where to place a
window centered on the screen. That is, 1/3th of the desktop shows above
the window and 2/3ths below it.  It returns a new rectangle set to the
centered position.  The top and left of this rectangle can be used with
MoveWindow.  This routine only considers the main device.  CenterWindowRect
could take a GDevice as a parameter to center the VAR rect with.  This
would also allow Alerts or dialogs that are closely associated with a
document window to be centered relative to the monitor that contains that
document.  This is also one of the Human Interface Guidelines.  MacApp 2.0
has a utility CalcScreenRect that you can borrow from to include this.
 
WARNING: This routine may move or purge memory.
*/
 
#pragma segment Open
void CenterWindowRect(short variant, Rect *theRect)
{
    Point rectSize;
    short wTitleHeight;
 
    wTitleHeight = GetWTitleHeight(variant);        //get title height
    SetPt(&rectSize, theRect->right, theRect->bottom + wTitleHeight);   //get size of rect
    SubPt(TopLeft(*theRect), &rectSize);            // include it in size
 
    theRect->top = ((qd.screenBits.bounds.bottom - qd.screenBits.bounds.top - GetMBarHeight() - rectSize.v)
                        / 3) + GetMBarHeight();     //1/3th below menubar, centered horz
    theRect->left = ((qd.screenBits.bounds.right - qd.screenBits.bounds.left) - rectSize.h) / 2;
 
    SetPt(&BottomRight(*theRect), theRect->left, theRect->top); //return adjusted rect
    AddPt(rectSize, &BottomRight(*theRect));
    theRect->top += wTitleHeight;                   //remove title height from rect
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Given a window ID, this routine will center the windowÕs rectangle before
showing it on the main screen.  This follows the Apple Human Interface
Guidelines for where to place a centered window on the screen.  If the
window is closely associated with another window, considerations should be
given to where that other window is located.  If this other window is on a
screen other then the main monitor, then it would be best to center the
window on that other monitor.
 
VERSION 1.1: There is a problem with GetNewWindow in the old Mac Plus and
SE ROMS.  It will call ReleaseResource on the 'WIND' resource within
GetNewWindow.  This make the handle invalid after the call.  Therefore,
before calling HPurge, we get the resource once again.
*/
 
#pragma segment Open
WindowPtr GetCenteredWindow(short id, Ptr p, WindowPtr behind)
{
    Rect newRect;
    WindowTHndl windTemplate;
    WindowPtr window;
 
    window = nil;                                               //initialize result
    windTemplate = (WindowTHndl)(Get1Resource('WIND', id));
    if (windTemplate != nil) {
        HNoPurge((Handle)windTemplate);
        newRect = (**windTemplate).boundsRect;
        CenterWindowRect((**windTemplate).procID, &newRect);
        (**windTemplate).boundsRect = newRect;
        window = GetNewWindow(id, p, behind);
        HPurge(Get1Resource('WIND', id));
    }
    return(window);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Given an Alert ID, this routine will center the alertÕs rectangle before
showing it on the main screen.  This follows the Apple Human Interface
Guidelines for where to place a centered window on the screen.  If the
Alert is closely associated with another window, considerations should be
given to what the window is located.  If this other window is on a screen
other then the main monitor, then it would be best to center the Alert on
that other monitor.
 
VERSION 1.2: System 7 supports auto-positioning of windows and dialog, so
this routine isn't being called anymore. I has been left in the sources
for the readers that may be interested in knowing how it works.
*/
 
#pragma segment Open
short CenteredAlert(short alertID)
{
    AlertTHndl alertHandle;
    Rect alertRect;
 
    alertHandle = (AlertTHndl)(Get1Resource('ALRT', alertID));
    if (alertHandle != nil) {
        HNoPurge((Handle)alertHandle);
        alertRect = (**alertHandle).boundsRect;
        CenterWindowRect(dBoxProc, &alertRect);
        (**alertHandle).boundsRect = alertRect;
        HPurge((Handle)alertHandle);
    }
    return(Alert(alertID, nil));
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Given a dialog ID, this routine will center the dialogÕs rectangle before
showing it on the main screen.  This follows the Apple Human Interface
Guidelines for where to place a centered window on the screen.  If the
dialog is closely associated with another window, considerations should be
given to what the window is located.  If this other window is on a screen
other then the main monitor, then it would be best to center the dialog on
that other monitor.
 
VERSION 1.1: There is a problem with GetNewDialog in the old Mac Plus and
SE ROMS.  It will call ReleaseResource on the 'WIND' resource within
GetNewWindow.  This make the handle invalid after the call.  Therefore,
before calling HPurge, we get the resource once again.
 
VERSION 1.2: System 7 supports auto-positioning of windows and dialog, so
this routine isn't being called anymore. I has been left in the sources
for the readers that may be interested in knowing how it works.
*/
 
#pragma segment Open
DialogPtr GetCenteredDialog(short id, Ptr p, WindowPtr behind)
{
    Rect newRect;
    DialogTHndl dlogTemplate;
    DialogPtr dialog;
 
    dialog = nil;                                               //initialize result
    dlogTemplate = (DialogTHndl)Get1Resource('DLOG', id);
    if (dlogTemplate != nil) {
        newRect = (**dlogTemplate).boundsRect;
        CenterWindowRect((**dlogTemplate).procID, &newRect);
        (**dlogTemplate).boundsRect = newRect;
        HNoPurge((Handle)dlogTemplate);
        dialog = GetNewDialog(id, p, behind);
        HPurge(Get1Resource('DLOG', id));
    }
    return(dialog);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Given any control handle, this will draw an outline around it.  This is
used for the default button of a window.  The extra nice feature here is
that IÕll erase the outline for buttons that are inactive.  Seems like
there should be a Toolbox call for getting a controlÕs hilite state.
Since there isnÕt, I have to look into the control record myself.  This
should be called for update and activate events.
 
The method for determining the oval diameters for the roundrect is a
little different than that recommended by Inside Mac.  IM I-407 suggests
that you use a hardcoded (16,16) for the diameters. However, this only
looks good for small roundrects. For larger ones, the outline doesnÕt
follow the inner roundrect because the CDEF for simply buttons doesnÕt
use (16,16). Instead, it uses half the height of the button as the
diameter. By using this formula, too, our outlines look better.
 
WARNING: This will set the current port to the controlÕs window.
*/
 
#pragma segment Main
void DoButtonOutline(ControlHandle button)
{
    Rect theRect;
    PenState curPen;
    short buttonOval;
 
    if (button != nil) {
        SetPort((**button).contrlOwner);
        GetPenState(&curPen);
        PenNormal();
        theRect = (**button).contrlRect;
        InsetRect(&theRect, kButtonFrameInset, kButtonFrameInset);
        buttonOval = (theRect.bottom - theRect.top) / 2;
        if (((**button).contrlHilite == kControlNoPart))
            PenPat(&qd.black);
        else
            PenPat(&qd.gray);
        PenSize(kButtonFrameSize, kButtonFrameSize);
        FrameRoundRect(&theRect, buttonOval, buttonOval);
        SetPenState(&curPen);
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Given the button control handle, this will cause the button to look as
if it has been clicked in.  This is nice to do for the user if they type
return or enter to select the default item.
*/
 
#pragma segment Main
void SelectButton(ControlHandle button)
{
    unsigned long finalTicks;
 
    HiliteControl(button, kHiliteControlSelect);
    Delay(kDelayTime, &finalTicks);
    HiliteControl(button, kHiliteControlDeselect);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Given a sound document, this routine goes through the control list looking
for a control that needs to be activated, or deactivated what ever the
case maybe.  The Play button is the default button, and it also needs its
outline drawn.  The Stop button is always active on the front most window.
This is true even if there is no selection made in the windowÕs list.  The
Stop button is a global action, regardless of what is happening in the window.
 
VERSION 1.1:  Stop button is only active while a sound is playing.
*/
 
#pragma segment Main
void ActivateSndCntls(SndDocPeek sndDoc)
{
    ControlHandle control;
    long cntlRefCon;
    Boolean activate;
 
    activate = ((WindowPeek)sndDoc)->hilited && HasSelection(sndDoc);
    control = (ControlHandle)((WindowPeek)sndDoc)->controlList;
    while (control != nil) {
        cntlRefCon = GetControlReference(control);
        if (    (cntlRefCon == rPlaySndCntl)
             || (cntlRefCon == rHyperPlayCntl)
             || (cntlRefCon == rPlayScaleCntl)
             || (cntlRefCon == rMelodyCntl)) {
            if (activate)
                HiliteControl(control, kControlNoPart);
            else
                HiliteControl(control, kControlInactivePart);
            if (cntlRefCon == rPlaySndCntl)
                DoButtonOutline(control);
        }
        if (cntlRefCon == rStopCntl) {
            if (((WindowPeek)sndDoc)->hilited && HasChannelOpen())
                HiliteControl(control, kControlNoPart);
            else
                HiliteControl(control, kControlInactivePart);
        } //cntlRefCon == rStopCntl
        if (cntlRefCon == rRecordCntl) {
            if (((WindowPeek)sndDoc)->hilited)
                HiliteControl(control, kControlNoPart);
            else
                HiliteControl(control, kControlInactivePart);
        } //cntlRefCon == rRecordCntl
        control = (**control).nextControl;
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Check if a window belongs to a desk accessory.  This will first test for a
nil window.  DAs will have a negitive windowKind.
*/
 
#pragma segment Main
Boolean IsDAWindow(WindowPtr window)
{
    if (window == nil)
        return(false);
    else    //DA windows have negative windowKinds
        return(((WindowPeek)window)->windowKind < 0);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Check to see if a window is a document window.  This will first test for a
nil window.  Document windows all have a refCon set to rSoundWindow.  This
insures there is document type information after the window record.
*/
 
#pragma segment Main
Boolean IsDocWindow(WindowPtr window)
{
    Boolean result;
 
    if (window == nil)
        result = false;
    else
        result = (GetWRefCon(window) == rSoundWindow);
    return(result);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This test will return true for a window that needs to be treated as a
modal window.  To include additional windows, add the windowÕs refCon
value to the set of modal windows.
*/
 
#pragma segment Main
Boolean IsModalWindow(WindowPtr window)
{
    long    wRef;
    Boolean result;
 
    if (window == nil)
        result = false;
    else {
        wRef = GetWRefCon(window);
        result = (wRef == rAboutWindow);        // test for all our modal windows
    }
    return(result);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This is used to free up all the sound data and channels in use.  There are
two reasons for using this routine.  One is after a sound has completed
the other is to terminate a sound that may be in progress.  For this
second reason, this routine should always be used before playing a new
sound.  When disposing of a sound channel, this sets all documentÕs
sndInUse flags to false.  I can call this routine at any time.  When I
find that the memory reserve is used, I will call this to free up any
possible sound resources which tend to be large.  Another important point
is the human interface issue of hiding the status window.  ItÕs possible
that  playing some short sounds could cause the status window to disappear
before the user had much of a chance to see it.  To solve this, there is a
delay that insures the status window was visible for a minimal time.
 
VERSION 1.1:  Update the document's controls since know that the sound
is no longer playing the buttons in the document window need to be updated.
*/
 
#pragma segment Main
void KillSound(void)
{
    WindowPtr window;
 
    if (HasChannelOpen()) {                         // if a channel is open...
        window = FrontWindow();                     // set all documentÕs flags
        while (window != nil) {
            if (IsDocWindow(window))                // if this is my document...
                ((SndDocPeek)window)->sndInUse = false; // then it is no longer active
            window = (WindowPtr)((WindowPeek)window)->nextWindow;
        }
    }
    if (HasSoundCompleted()) {                      // why were we called?
        DoSoundComplete();                          // from completion
 
        // delay for status window to show a minimal amount of time
        while ((TickCount() - gStatusWindow->showTime) < kShowTimeDelay) {};
    }
    else
        FreeAllChans();                             // or just because
    HideWindow((WindowPtr)gStatusWindow);
    window = FrontWindow();
    if (IsDocWindow(window))
        ActivateSndCntls((SndDocPeek)window);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Close any of my windows.  At this point, if there was a document
associated with a window, you could do any document saving processing if
it has been changed since being opened.  DoCloseWindow would return true
if the window actually closed.  false would be returned, as an example, if
the user chose ÒCancelÓ in the Save AsÉ dialog.  An important detail is if
the window to be close is one of my documents that is currently playing a
sound.  We cannot dispose of it yet, since the sound resource owned by the
document would be in use by the Sound Manager.  So, I test the documentÕs
sndInUse flag and if it is in use I call KillSound.  The About window
contains some resource handles.  I only mark them purgeable, but I could
use ReleaseResource and then DisposHandle.
*/
 
#pragma segment Main
Boolean DoCloseWindow(WindowPtr window)
{
    Boolean     result = true;
 
    switch (GetWRefCon(window)) {
 
        case rSoundWindow:
            if (((SndDocPeek)window)->sndInUse)     // contain a snd in use?
                KillSound();                        // not any more
            if (((SndDocPeek)window)->list != nil)  // dispose of document data
                LDispose(((SndDocPeek)window)->list);
            if (((SndDocPeek)window)->resFile != 0)
                CloseResFile(((SndDocPeek)window)->resFile);
            CloseWindow((WindowPtr)window);
            DisposePtr((Ptr)window);                // dispose of our doc storage
            break;
 
        case rStatusWindow:
            DisposeHandle((Handle)gStatusWindow->message);
            CloseWindow((WindowPtr)gStatusWindow);
            DisposePtr((Ptr)gStatusWindow);
            break;
 
        case rAboutWindow:
            if (((AboutWPeek)window)->comment != nil)   // never dispose...
                HPurge(((AboutWPeek)window)->comment);  // a Resource handle
            if (((AboutWPeek)window)->appPict != nil)
                HPurge(((AboutWPeek)window)->appPict);
            CloseWindow((WindowPtr)window);
            DisposePtr((Ptr)window);
            break;
 
        default:
            if (IsDAWindow(window))                     // we can close DAs too
                CloseDeskAcc(((WindowPeek)window)->windowKind);
            break;
 
    } // switch GetWRefCon(window)
    return(result);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Display an alert to inform the user of an error.  MessageID acts as an
index into a STR# resource of error messages.  This will attempt to
replace the error number with a string if the sound is a Sound Manager
error.
 
BUG NOTE: GetIndString will return a bogus string if the index is not
positive.
*/
 
#pragma segment Main
void AlertUser(short error, short messageID)
{
    Str255  msg1;
    Str255  msg2;
    short   theItem;
 
    UseResFile(gAppResRef);                     // restore our resource file
    if (messageID > 0)
        GetIndString(msg1, sErrStrings, messageID);
    else
        msg1[0] = 0;                            // case thereÕs no message
    if ((error <= noHardware) && (error >= badFormat))
        GetIndString(msg2, sSMErrStrings, AbsoluteValue(error) + noHardware + 1);
    else
        NumToString(error, msg2);
    ParamText(msg1, msg2, "\p", "\p");
    theItem = Alert(rUserAlert, nil);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Display an alert that tells the user an error occurred, then exit the
program.  This routine is used as an ultimate bail-out for serious errors
that prohibit the continuation of the application.  Errors that do not
require the termination of the application are handled with AlertUser.
Error checking and reporting has a place even in the simplest application.
 
BUG NOTE: GetIndString will return a bogus string if the index is not
positive.
*/
 
#pragma segment Main
void EmergencyExit(short message)
{
    Str255  msg;
    short   theItem;
 
    SetCursor(&qd.arrow);
    if (message > 0)
        GetIndString(msg, sErrStrings, message);
    else
        msg[0] = 0;                             // case thereÕs no message
    ParamText(msg, "\p", "\p", "\p");
    theItem = Alert(rExitAlert, nil);
    ExitToShell();                              //gotta go!
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Clean up the application and exit.  I close all of the windows so that
they can update their documents, if any.  Dispose of the SoundUnit and
close the status window if the user really wants to quit.
*/
 
#pragma segment Main
void Terminate(void)
{
    WindowPtr aWindow;
    Boolean closed;
 
    closed = true;
    KillSound();                                    //stop any sound in progress
    do {
        aWindow = FrontWindow();                    //get the current front window
        if (aWindow != nil)
            closed = DoCloseWindow(aWindow);        //close this window
    } while ((closed) && (aWindow != nil));         //do all windows
    if (closed) {
        FreeSoundUnit();                            //get rid of all sound equipment
        gTerminate = true;                          //exit if no cancellation
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Draw the status message and update the button.  I setup a rectangle that
is the text area.  This is the entire window area, inset a small amount.
I also subtract from the bottom of the rectangle the area used by the button.
Doing things this way allowed me to change the size of the window and
not change any of this code.  The rectangle used by text will fill the
window regardless of the new size I give it in the WIND resource.
*/
 
#pragma segment Main
void DrawStatusWindow(void)
{
    Rect    theRect;
 
    PenNormal();
    theRect = ((WindowPtr)gStatusWindow)->portRect;
    theRect.bottom = theRect.bottom - kDafaultButSizeH;
    InsetRect(&theRect, kStadardWhiteSpacing, kStadardWhiteSpacing);
    HLock((Handle)gStatusWindow->message);
    TETextBox((*(gStatusWindow->message) + 1),
                **(gStatusWindow->message), &theRect, teJustLeft);
    HUnlock(((Handle)gStatusWindow->message));
    UpdateControls((WindowPtr)gStatusWindow, ((WindowPtr)gStatusWindow)->visRgn);
    DoButtonOutline((ControlHandle)((WindowPeek)gStatusWindow)->controlList);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Whenever I start a sound, I show this status window.  It will contain a
message showing what is currently happening.  A very good rule is not to
perform drawing outside of the update event.  I bend that rule here.  I
could simply select this window and show it.  This would allow my normal
drawing and activating routines to be called.  The problem with that was,
I found that many times the status window appeared and then disappeared
before these update routines had a chance to draw the window.  This left
the user with a blank window that immediately went away.  So, I do the
activation and drawing right after showing it.
 
BUG NOTE: GetIndString will return a bogus string if the index is not
positive.
*/
 
#pragma segment Main
void ShowStatusWindow(short messageID)
{
    Str255  msg;
 
    if (FailLowMemory(0)) {                         // running low on memory?
        KillSound();
        AlertUser(memFullErr, sLowMemory);
    }
    else {
        if (messageID > 0)
            GetIndString(msg, sMsgStrings, messageID);
        else
            msg[0] = 0;                                 // case thereÕs no message
        SetString(gStatusWindow->message, msg);
        ShowWindow((WindowPtr)gStatusWindow);           // show the window
        SelectWindow((WindowPtr)gStatusWindow);         // bring it to the front
        SetPort((WindowPtr)gStatusWindow);
        EraseRect(&(((WindowPtr)gStatusWindow)->portRect)); // erase old message
        HiliteControl((ControlHandle)((WindowPeek)gStatusWindow)->controlList, kControlNoPart);
        DrawStatusWindow();                             // draw the new message
        ValidRect(&(((WindowPtr)gStatusWindow)->portRect)); // avoid needless update
        gStatusWindow->showTime = TickCount();          // itÕs show time folks
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This initializes the status window, and keeps it hidden until the user
decides to play a sound.  I also center it for the first time, but the
user is free to drag it to a new location afterwards.  The status window
also has a message associated to it, so I allocate a string handle for this.
*/
 
#pragma segment Initialize
void InitStatusWindow(void)
{
    WindowPtr       window;
    ControlHandle   stopButton;
 
    gStatusWindow = (StatWindowPeek)NewPtrClear(sizeof(StatusWindow));
    if (gStatusWindow == nil)
        EmergencyExit(sInitStatusErr);
    window = GetNewWindow(rStatusWindow, (Ptr)gStatusWindow, (WindowPtr)-1);
    if (window == nil)
        EmergencyExit(sInitStatusErr);
    SetWRefCon(window, rStatusWindow);
    stopButton = GetNewControl(rCancelCntl, window);
    if (stopButton == nil)
        EmergencyExit(sInitStatusErr);
    gStatusWindow->message = NewString("\p");   // a new empty string handle
    if (gStatusWindow->message == nil)
        EmergencyExit(sInitStatusErr);
    HNoPurge((Handle)gStatusWindow->message);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
I just flash the menubar for a moment instead of playing any sounds.  ItÕs
the same effect as setting the sound volume to zero but that would prevent
me playing my sounds.  Also, it helps to prevent the problem of _SysBeep
being called by the Dialog Manager while IÕm playing a sound.  If my
application is playing a sound and, for example, the user clicks outside
of the Standard File dialog ModalDialog calls _SysBeep.  This can cause
the Mac to crash or trash my channel.
 
BUG NOTE: If the current Sound Manager were playing a sound and a
_SysBeep were to occur, bad things could happen on a Mac Plus/SE.  Either
the applicationÕs channel would be trashed or the Mac could crash.
 
VERSION 1.1: The new Sound Manager will handle the problem presented in the
older Sound Manager regarding SysBeep.  If I'm running under the new Sound
Manager then I'll call SysBeep anyway.  The new Sound Manager will properly
handle the situation of an open sound channel when SysBeep is called.
 
VERSION 1.2: This is no longer needed since we only use Sound Manager 2 or later.
*/
 
#pragma segment Main
pascal void DoErrorSound(short soundNo)
{
    unsigned long finalTicks;
 
    if (GetSoundMgrVersion() == 1) {
        if (soundNo > 0) {                      //only after the first time
            FlashMenuBar(0);
            Delay(kDelayTime, &finalTicks);
            FlashMenuBar(0);
        }
    }
    else
        SysBeep(30);                            //does the right thing now
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This tests if the volume has been set very low.  ItÕs a judgement call as
to what Òtoo lowÓ means.  The user sets the volume, so how could it be too
low? A setting of 0 could be considered too low, but in any case do not
adjust it.  I find the volume goes below 4 on a standard Mac, then there
is a perceivable difference.  I also wanted to show this routine as a
demonstration in how to handle such a situation.  Do not change it
directly! Ask the user to do it.  Consider the user that has their Mac
connected to a stack of Marshalls (which go to 11) and has purposely set
their MacÕs volume where they wanted it.  This may be 3 and if you crack
it up to 8, it could be a scene out of Back to the Future.
 
User Interface rule, let the user remain in control!
 
VERSION 1.2: Don't use GetSoundVol anymore. This just reads a low memory global
which is no longer valid with Sound Manager 3 or later. The old low global
is only 3 bits, which is why the range used to be 0-7. There are new machines
supported by Sound Manager 3 that have more than 8 volume level. So the
old global is scaled into the 0-7 range. You should avoid all of this non-sense
and start using the new calls of Sound Manager 3, and start using percentages.
*/
 
#pragma segment Initialize
void CheckSoundVolume(void)
{
    Fixed   percent;
    long    soundVolume;
    short   curVolume;
    short   item;
    Str255  numStr;
 
    if (GetSoundMgrVersion() < 3)
    {
        // turn old 0-7 range into the new 0-0x0100 range
 
        curVolume = LMGetSdVolume();                // same as calling GetSoundVol()
        curVolume *= kFullVolume;
        curVolume += 7-1;                           // avoid rounding errors
        curVolume /= 7;                             // divid by old max
    }
    else
    {
        // get the current stereo levels and average them
 
        GetDefaultOutputVolume(&soundVolume);
        curVolume = (soundVolume >> 16) + (soundVolume & 0x0000FFFF);
        curVolume /= 2;
    }
 
    // take the current level and turn this in to a percentage of full volume,
    // then show the user that the level is low and how they may increase it.
 
    percent = FixDiv( (long)curVolume << 16, kFullVolume << 16);
    percent = FixMul(percent, 100L << 16);
    NumToString(percent >> 16, numStr);
    ParamText(numStr, "\p", "\p", "\p");        // show the current volume
    item = Alert(rSoundVolAlert, nil);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Simply add all the buttons I want to use in the document window.  I get
the control templates, and add the control to the window.  I donÕt check
for getting errors for two reasons.  I put these controls there.  If the
user removes them, itÕs their problem.  If the application cannot get the
memory required for these, then the application is already out of memory
and checking here probably wouldnÕt help much.  I do check before creating
the document that there was enough available memory.  I save the resource
ID into the controlÕs refCon so later I can tell which control was hit by
the user.
 
VERSION 1.1: Add the record button to the window.}
*/
 
#pragma segment Open
void AddSndDocControls(WindowPtr window)
{
    ControlHandle   control;
 
    control = GetNewControl(rPlaySndCntl, window);
    if (control != nil)
        SetControlReference(control, rPlaySndCntl);
 
    control = GetNewControl(rHyperPlayCntl, window);
    if (control != nil)
        SetControlReference(control, rHyperPlayCntl);
 
    control = GetNewControl(rPlayScaleCntl, window);
    if (control != nil)
        SetControlReference(control, rPlayScaleCntl);
 
    control = GetNewControl(rMelodyCntl, window);
    if (control != nil)
        SetControlReference(control, rMelodyCntl);
 
    control = GetNewControl(rStopCntl, window);
    if (control != nil)
        SetControlReference(control, rStopCntl);
 
    control = GetNewControl(rRecordCntl, window);
    if (control != nil)
        SetControlReference(control, rRecordCntl);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#pragma segment Open
Boolean PositionAvailable(Point newPt, short wTitleHeight)
{
    WindowPtr oldWindow;
    Point delta;
    Point oldPt;
    Boolean taken;
 
    taken = false;
    oldWindow = FrontWindow();
    while ((oldWindow != nil) && !taken) {
        if (IsDocWindow(oldWindow) && ((WindowPeek)oldWindow)->visible) {
            oldPt = GetGlobalTopLeft(oldWindow);
            delta.v = AbsoluteValue(newPt.v - oldPt.v);
            delta.h = AbsoluteValue(newPt.h - oldPt.h);
            taken = (delta.h + delta.v)
                        <= ((kWindowPosStaggerH + kWindowPosStaggerV + wTitleHeight) / 2);
        }
        oldWindow = (WindowPtr)(((WindowPeek)oldWindow)->nextWindow);
    }
    return(!taken);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
There are a few approaches I thought of and tried, none of which satisfied
me.  ResEdit uses the simplest method, an offset from the front window.  I
donÕt like this because it may cover up existing windows.  So my approach is
to go through the window list looking for an empty spot.  If the empty
spot causes the window to go off the screen, then I start a new stack off
to the right.  This will cause a new series of window to be stacked.  If the
new stack would cause the window to go off screen, then itÕs time to give up.
All new windows will then be moved to the starting point and there is no
further stacking.  If I look at this routine too much, I get real tired.
If I try to change it at all, I get woozy trying to make it work again.
 
VERSION 1.2: System 7 provides for staggering windows, but it doesn't work
exactly the way I want it. Mine's better.
*/
 
#pragma segment Open
WindowPtr NewStackedWindow(short windID, Ptr windStorage)
{
    Point windSize;
    Point newPt;
    WindowPtr newWindow;
    short wTitleHeight;
    short numStacks;
    Boolean taken;
 
    //set the initail starting point for a window, less the staggering amount
    newWindow = GetNewWindow(windID, windStorage, (WindowPtr)-1);
    windSize = BottomRight(newWindow->portRect);
    SubPt(TopLeft(newWindow->portRect), &windSize);
    wTitleHeight = GetWTitleHeight(GetWVariant(newWindow));
    SetPt(&newPt, kWindowPosStartPt - kWindowPosStaggerH, kWindowPosStartPt - kWindowPosStaggerV + GetMBarHeight());
 
    numStacks = 0;
    do { //add the staggering amount then test if the window goes off the screen
        taken = true;
        SetPt(&newPt, newPt.h + kWindowPosStaggerH, newPt.v + kWindowPosStaggerV + wTitleHeight);
        if (    ((newPt.v + windSize.v) > qd.screenBits.bounds.bottom)
             || ((newPt.h + windSize.h) > qd.screenBits.bounds.right))
        {
            numStacks++;
            SetPt(&newPt, kWindowPosStartPt + (numStacks * kWindowPosStaggerH),
                            kWindowPosStartPt + wTitleHeight + GetMBarHeight());
            if (((newPt.v + windSize.v) > qd.screenBits.bounds.bottom)
                        || ((newPt.h + windSize.h) > qd.screenBits.bounds.right)) {
                SetPt(&newPt, kWindowPosStartPt, kWindowPosStartPt + wTitleHeight + GetMBarHeight());
                taken = false;
            }
        }
    } while (taken && !(PositionAvailable(newPt, wTitleHeight)));
 
    MoveWindow(newWindow, newPt.h, newPt.v, true);
    return(newWindow);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Create a new sound document and initialize all the data associated with
it.  If I encounter an error, then dispose of any memory allocated in the
attempt.  Allocate all necessary memory required for a sound document.  I
like to use NewPtrClear because is will initialize all the memory.  I
depend on real values or zero in my document fields.  Then its time to
build a sound document which contains all the controls and the list of
sounds.  I get the position for the list kept in a rectangle resource.
This  helps me to adjust the size of the list without changing code.  I
will make  an additional adjustment to the listÕs height to insure better
scrolling.  This avoids the ugly white space of an improperly sized
rectangle.  If I cannot create the list, IÕll close the window and return
a NIL.  If I find after creating a new document that IÕm low on memory,
IÕll close it and return an error.
 
SPECIAL NOTE: The Human Interface Group suggested that the buttons and the
list should in the System font.  I designed these windows using the
application font in 10 point.  If you change the call to TextFont, you
find that the buttons and the list will work properly in your font choice.
 
VERSION 1.1:  Size the window smaller to hide the record button from the
user if Sound Input is not available.  Added memory check to the beginning
of this routine before creating the document.  This was done because there
are two routines that call this one, OpenSoundDoc and NewSoundDoc which
both performed this check.  Having it here in one place reduces the chance of
future bugs (doing the same thing in two places) and reduces the size of
the code.
*/
 
#pragma segment Open
OSErr CreateSoundDoc(short resRef, FSSpecPtr file)
{
    SndDocPeek newDocPtr;
    WindowPtr window;
    ListHandle list;
    RectHandle rHandle;
    Rect lView;
    Rect lBounds;
    Cell aCell;
    Point cSize;
    Point newSize;
    short maxListHeight;
    OSErr theErr;
    Boolean ignore;
 
    if (FailLowMemory(kMemForSndDoc)) {
        CloseResFile(resRef);                           //close it before leaving
        return(memFullErr);
    }
 
    theErr = noErr;
    newDocPtr = (SndDocPeek)NewPtrClear(sizeof(SndDocument));
    if (newDocPtr != nil) {
        window = NewStackedWindow(rSoundWindow, (Ptr)newDocPtr);
        SetPort(window);
        if (! HasSoundInput())
            SizeWindow(window, kSoundWindowSizeW,
                    kSoundWindowSizeH - (kStadardWhiteSpacing + kSndButtonSizeH), false);
        TextFont(0);                                    //set window to System font, blech
        AddSndDocControls(window);                      //add my buttons
        SetWRefCon((WindowPtr)newDocPtr, rSoundWindow); //mark as an app window
        SetWTitle((WindowPtr)newDocPtr, file->name);
        newDocPtr->resFile = resRef;                    //save its resource file ref
        newDocPtr->vRefNum = file->vRefNum;             //save its volume reference
        newDocPtr->dirID = file->parID;                 //save its directory ID
        newDocPtr->sndInUse = false;                    //not yet it doesnÕt
        rHandle = (RectHandle)Get1Resource('RECT', rListRectID);
        if (rHandle != nil) {                           //get the stored list size
            lView = **rHandle;
            SetRect(&lBounds, 0, 0, 1, 0);              //one dimentional list
            SetPt(&cSize, 0, 0);                        //List Mgr will find cell size
            list = LNew(&lView, &lBounds, cSize, 0, window, false, false, false, true);
            maxListHeight = window->portRect.bottom - window->portRect.top - (2 * kStadardWhiteSpacing);
            if (maxListHeight < (lView.bottom - lView.top))
                maxListHeight = (lView.bottom - lView.top);
            if (list != nil) {
                newSize.h = (**list).cellSize.h;        //get the width of one cell
                newSize.v = maxListHeight;              //get the best height for all cells
                newSize.v -= maxListHeight % (**list).cellSize.v;
                LSize(newSize.h, newSize.v, list);      //adjust for best scrolling
                (**list).selFlags = lOnlyOne;           //single selections only
                newDocPtr->list = list;                 //save the list handle
                theErr = InitSndList(newDocPtr);        //initialize the list data
                if (theErr == noErr) {
                    SetPt(&aCell, 0, 0);                //by default, I will...
                    LSetSelect(true, aCell, newDocPtr->list); //select first item
                    LSetDrawingMode(true, newDocPtr->list);
                    ShowWindow((WindowPtr)newDocPtr);   //get the show on the road
                    if (FailLowMemory(0))               //if IÕm low on memory...
                        theErr = memFullErr;
                }
            }
            else
                theErr = nilHandleErr;                  //list handle was NIL
        }
        else
            theErr = nilHandleErr;                      //RECT handle was NIL
        if (theErr != noErr) {                          //could not create the list
            ignore = DoCloseWindow(window);             //if not, close the window
            newDocPtr = nil;                            //return nil pointer too
        }
    }
    else                                                //NewPtrClear was nil
        theErr = MemError();
    return(theErr);                                     //return the error, if any
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Believe it or not, but this turned out to be one of the more difficult
routines to write.  This was the best approach I could thing of to avoid
serious problems.  I donÕt center the Standard File dialog because it
wouldnÕt be under the File menu where the user probably has the mouse.  I
use OpenRFPerm because it allows for permissions and doesnÕt depend on the
current directory.  First problem with opening a resource file was to
check if the file already being open.  Read Tech Notes #116 and #185.
GetFInfo seems to do the trick.  At least it does report all files that
are open on this machine.  It may be incorrect for files on AppleShare
opened by other machines.  What IÕm worried about is when opening a
resource file for the second time on the same machine may return the
previous openerÕs resource map.  This is very dangerous, and if the second
opener calls CloseResFile there will be a crash.  I want to be friendly
and if the user tries to open the file the second time and IÕve already
got it on the screen, then IÕll bring that window forward.  To test for
this, I compare the fileÕs name, DirID and vRefNum.  The vRefNum is
dynamic and needs to be converted into a volume name if I were to save
this information for future use.
 
There are a couple pitfalls, even with this method.  The user can open the
file, then move it to another directory.  This same thing confuses all
other applications IÕve tested.  Opening a resource file will allocate a
resource map handle into my heap, which may be large depending on the
number of resources in that file.  Additionally this loads all resources
marked Òpreload.Ó  So I set ResLoad to false to prevent the preloading.  I
test if the amount of memory I believe my document will require is
available after opening the resource file.  If true, I test if there are
any sound resources in that file and if not alert the user.  After all
this, create the new document.  If after creating the new document I find
that memory is too low, will close it and return an error.  If it does
fail, then close the file before anything else or there may not be enough
memory to show the alert.  All of these tests created a number of
IF-THEN-ELSE blocks and became unyielding.  C programmers get a break, so
gimme me one too.  I use the MPW Pascal EXIT.
 
BUG NOTE: DonÕt open a resource file that is already open.  OpenResFile
may return an existing resource map when it gets opWrErr from the file
system.  If this happens, the resource file will not be unique and this
is very bad.  Another problem is if I get a read-only path and someone
else opens it for read/write.  This is also very bad.  Read Tech Notes
#116 and #185 hint at this problem, but I think a more comprehensive one
is in order.
 
BUG NOTE: GetWDInfo fails with nsvErr if the working directory returned
from Standard File is the root of an A/UX volume.  I could work around
this,  but it would be dependant on the current version of A/UX.  Read
Tech Note #229.  I believe this is also true for TOPS.
 
VERSION 1.1:  I'll open files that do not have snd resources in them.
This allows users to open existing files and then record or paste a sound
into it.  I changed OpenRFPerm to HOpenResFile to avoid working
directories and to be consistent with the rest of the sources.  I
separated the standard file code from OpenSndDoc to support opening
documents being open from the Finder.  This allows me to share the same
routine to open files either from double clicking them in the Finder, or
by the Standard File dialog.  Removed the checking for low memory conditions
since CreateSoundDoc is now doing this.
*/
 
#pragma segment Open
void OpenSoundDoc(FSSpecPtr file)
{
    HParamBlockRec fPBRec;
    WindowPtr window;
    short resFileRef;
    OSErr theErr;
 
    fPBRec.fileParam.ioCompletion = nil;            //prepare a paramBlock
    fPBRec.fileParam.ioNamePtr = file->name;
    fPBRec.fileParam.ioVRefNum = file->vRefNum;
    fPBRec.fileParam.ioDirID = file->parID;
    fPBRec.fileParam.ioFVersNum = 0;
    fPBRec.fileParam.ioFDirIndex = 0;
    theErr = PBHGetFInfoSync(&fPBRec);              //fPBRec on stack, synch only
    if (theErr == noErr) {
        if (fPBRec.fileParam.ioFlAttrib & kResForkOpenBit) {
            if (OpenByApp(file, &window))
                SelectWindow(window);               //I opened this one, select it
            else
                AlertUser(noErr, sCurInUseErr);     //in use by someone else
            return;
        }
    } else {
        AlertUser(theErr, sStandardErr);            //PBGetFInfo failed
        return;
    }
    SetResLoad(false);                              //donÕt load any resources
    resFileRef = FSpOpenResFile(file, fsCurPerm);
    theErr = ResError();                            //save error, if any
    SetResLoad(true);                               //restore ResLoad state
    UseResFile(gAppResRef);                         //changes ResErr
    if (resFileRef != kResFileNotOpened)            //error from OpenRFPerm?
        theErr = CreateSoundDoc(resFileRef, file);
    if (theErr != noErr)
        AlertUser(theErr, sNewDocErr);              //couldnÕt create new doc
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
VERSION 1.1:  Create a new file for the user.  I prefer to use the new
HCreateResFile to avoid working directories, and to be consistent with
the rest of the sources.  Also, I need the real vRefNum and dirID to
store in the sndDoc record.  After creating the file, I set its Finder
information to the proper type and creator.  Finally, I get to open the
new file.  After this point everything is exactly like opening an
existing file and creating a new sound document.
 
VERSION 1.2: If dupFNErr is returned by HCreateResFile, then the user
wants to replace the file with a new SoundApp document.
*/
 
#pragma segment Open
void NewSoundDoc(void)
{
    Str255 msg;
    StandardFileReply reply;
    StringHandle strHandle;
    short resFileRef;
    OSErr theErr;
 
    strHandle = (StringHandle)Get1Resource('STR ', rPutFileMsg);
    if (strHandle != nil)
        PStringCopy(*strHandle, msg);                       //save no name title
    else
        msg[0] = 0;                                         //at least an empty string
 
    StandardPutFile(msg, "\p", &reply);
    if (reply.sfGood)
    {
        theErr = noErr;
        if (reply.sfReplacing)
            theErr = FSpDelete(&reply.sfFile);              //we're replacing it
        if (theErr == noErr)
        {
            FSpCreateResFile(&reply.sfFile, rAppSignature, rSndAppDocType, reply.sfScript);
            theErr = ResError();
            if (theErr == noErr) {
                resFileRef = FSpOpenResFile(&reply.sfFile, fsCurPerm);
                theErr = ResError();                        //save error, if any
                UseResFile(gAppResRef);                     //changes ResErr
                if (resFileRef != kResFileNotOpened)        //error from HOpenResFile
                    theErr = CreateSoundDoc(resFileRef, &reply.sfFile);
            }
        }
        if (theErr != noErr)
            AlertUser(theErr, sNewDocErr);                      //return the error
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This is much more than what a typical file filter might do but I wanted
the user to easily find files that contained snd resources.  So, the
global flag gSndFilesOnly is used to determine when to filter for such
files.  Otherwise, I show all files and allow the application to report to
the user the file theyÕve just tried to open doesnÕt have any sounds to
play with.  Opening resource forks can be tricky if itÕs already open.  It
would be very bad to use a resource fork that is already open by another
application.  The problem being that the Resource Manager doesnÕt deal
with multiple users.  Read Tech notes #116 and #185.  ThereÕs also another
problem.  If a resource fork is opened by two applications and one closes
the file then the entire resource fork may closed out from underneath the
other application.  I do, however, want to show the user files that
contain sound resources even if they are currently open.  I use
HOpenResFile with read only permission, which will give me a unique
resource reference.  I look for a 'snd ' resource and then immediately
close the file.  Do not use a resource file opened with read only
permission.  Another reason to use HOpenResFile  is to avoid a necessary
working directory.  I do not have one while in the file filter, but can
use the DirID.  Performing this search on each file is time consuming so I
show a spinning cursor to show the user IÕm working.   Opening a resource
fork may load resources mark preload.   To avoid this, I call SetResLoad
to false.  I bet you thought the Resource Manager was a free lunch.  Ha!
Read Tech Note #203 for other reasons not to play with resources.
 
BUG NOTE: While debugging this routine using heap scramble, I found that
OpenResFile would not open the requested file.  IÕm not sure if this is a
problem with OpenResFile or SFGetFile.
 
VERSION 1.1: The bug mentioned above was found.  The problem is that the
paramBlock happens to be a re-locatable block in the heap.  Passing the
de-reference ioNamePtr to HOpenResFile was de-referencing this
re-locatable paramBlock.  Since HOpenResFile moves memory, the namePtr
would no longer valid and thus HOpenResFile would fail.  Now I copy the
name out of the paramBlock and use the local variable in HOpenResFile.
This problem was fixed in System 7, which no longer passes a re-locatable
block to the file filter.  The new version allows for any resource file
to be opened.  Because of this new feature, I only show the user files
that have a resource fork.
 
VERSION 1.2: This is a filter for System 7 now, and I don't show invisible files.
*/
 
#pragma segment Open
pascal Boolean SFFilter(CInfoPBPtr p, Boolean *sndFilesOnly)
{
#define kShowIt         false               //false means I do not filter out...
#define kDoNotShowIt    true                //the file and true means that I do.
 
    long oldTicks;
    short resRef;
    short curRes;
    Boolean result;
 
    oldTicks = TickCount();
    result = kDoNotShowIt;                  //donÕt show anything until I say so
 
    if ( (*sndFilesOnly) && !(p->hFileInfo.ioFlFndrInfo.fdFlags & fInvisible) )
    {
        curRes = CurResFile();
        SetResLoad(false);
        resRef = HOpenResFile(p->hFileInfo.ioVRefNum, p->hFileInfo.ioFlParID,
                                p->hFileInfo.ioNamePtr, fsRdPerm);
        if (resRef != kResFileNotOpened) {
            UseResFile(resRef);
            if (Count1Resources(soundListRsrc) > 0)
                result = kShowIt;           //hey, we found a sound in here
            CloseResFile(resRef);
        }                                   //restore everything
        SetResLoad(true);
        UseResFile(curRes);
    } else
    {
        if (   !(p->hFileInfo.ioFlFndrInfo.fdFlags & fInvisible)    // if not invisible
             && (p->hFileInfo.ioFlRLgLen > 0) )                     // and has resources
        {
            result = kShowIt;
        }
    }
    //RotateCursor(TickCount() - oldTicks);
    return(result);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#pragma segment Open
pascal short SFGetHook(short mySFItem, DialogPtr dialog, void *sndFilesOnly)
{
    ControlHandle cntl;
    Rect r;
    short kind;
    short result;
 
    result = mySFItem;
    GetDialogItem(dialog, rSndOnlyCheckBox, &kind, (Handle *)&cntl, &r);
    switch (mySFItem) {
 
        case sfHookFirstCall:
            if (*(Boolean *)sndFilesOnly)
                SetControlValue(cntl, kCntlOn);
            else
                SetControlValue(cntl, kCntlOff);
            break;
 
        case rSndOnlyCheckBox:
            if (GetControlValue(cntl) == kCntlOff) {
                SetControlValue(cntl, kCntlOn);
                *(Boolean *)sndFilesOnly = true;
            }
            else {
                SetControlValue(cntl, kCntlOff);
                *(Boolean *)sndFilesOnly = false;
            }
            result = sfHookRebuildList;
            break;
 
    }
    return (result);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
VERSION 1.1: This routine used to be inside of OpenSoundDoc.  The
standard file code was removed to here in order to support opening
documents being open from the Finder.  I've switched to using a constant
for the topLeft point of the Standard File dialog.  It saves a few bytes
of code, so what the hell?  I found a cosmetic problem with the cursor
not being restored back to arrow after returning Standard File if there
was an error dialog shown immediately.  So, it is immediately set back to
the arrow.
*/
 
#pragma segment Main
void GetSoundDoc(void)
{
    StandardFileReply reply;
    SFTypeList typeList;                            //not used, just a placeholder
    Point sfTopLeft;
    Boolean sndFilesOnly;
 
    //SpinCursor(0);                                    //get the spinning cursor ready
    sfTopLeft.v = -1;                               //-1,-1 means to center the dialog
    sfTopLeft.h = -1;
    sndFilesOnly = false;
    CustomGetFile((FileFilterYDUPP)GetRoutineAddress(SFFilter), -1, typeList, &reply, rCustomGetFileDLOG,
            sfTopLeft, GetRoutineAddress(SFGetHook), nil, nil, nil, &sndFilesOnly);
    SetCursor(&qd.arrow);
    if (reply.sfGood)
        OpenSoundDoc(&reply.sfFile);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Draw the contents of the about window in response to an update event.  At
this point, BeginUpdate has been called which sets the windowÕs visRgn to
clip drawing only where it needs to be done.  I have some text to draw, an
icon, a picture, and a default button with its outline.  If I have a color
icon handle IÕll call PlotCIcon.  I offset the picture to draw to the
right of the icon.  The text will appear in a rectangle as large as the
window, but below the icon and above the button.  This allows me the
change the text and if needed change the size of the windowÕs rectangle to
compensate.  I wonÕt have to recompile any code.  Some people use dialogs
because of this, but IÕm demonstrating how it can be done without them.  I
use UpdateControls to avoid needless drawing that happens with DrawControls.
It not only runs faster but doesnÕt flicker.
 
VERSION 1.2: Uses System 7's PlotIcon to draw the proper icon.
*/
 
#pragma segment Main
void DrawAboutWindow(WindowPtr window)
{
    Rect theRect;
    OSErr ignoreErr;
 
    PenNormal();
    SetRect(&theRect, kStdAlertIconLeft, kStdAlertIconTop, kStdAlertIconRight, kStdAlertIconBottom);
    ignoreErr = PlotIconID(&theRect, atNone, ttNone, rMoofIcon);
 
    theRect = (**(PicHandle)((AboutWPeek)window)->appPict).picFrame;
    OffsetRect(&theRect, -theRect.left + kStdAlertIconRight + kStadardWhiteSpacing,
                            -theRect.top + kStdAlertIconTop);
    DrawPicture((PicHandle)((AboutWPeek)window)->appPict, &theRect);
 
    SetRect(&theRect, window->portRect.left, kStdAlertIconBottom,
                window->portRect.right,
                window->portRect.bottom - kDafaultButSizeH);
    InsetRect(&theRect, kStadardWhiteSpacing, kStadardWhiteSpacing);
    HLock(((AboutWPeek)window)->comment);
    TETextBox(*(((AboutWPeek)window)->comment) + 1,
                StrLength(*(((AboutWPeek)window)->comment)), &theRect, teJustLeft);
    HUnlock(((AboutWPeek)window)->comment);
 
    UpdateControls(window, window->visRgn);
    DoButtonOutline((ControlHandle)((WindowPeek)window)->controlList);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
First thing to do is call KillSound to stop any sound in progress and
close the status window.  If thereÕs enough memory available, IÕll play
the about sound.  This is sound is asynchronous and will automatically be
disposed of when completed.  There is an icon, taken from the bundle
resources, and a picture.  The text is taken from a string resource.  It
contains a couple variables just like ParamText would want.  I get the
current name of the application from the AppParms.  This is the name of
the application as specified by the user.  I read in my 'vers' resource
to get the current version to be displayed in the window.  Read Tech Note
189 for more details on the vers resource.  I think these are two
important things to show in the about box.  These two portions of the text
are put into place with my own version of ParamText.   I tried all of this
with a standard dialog, but had trouble.  Sometimes the color icon didnÕt
show up.  Trying to center justify text with a statText item wasnÕt really
possible.  On the Mac SE, for some unknown reason, TESetJust failed to
center the textH of the dialog.  I also found that this call would set the
dialogÕs text back to Chicago even after I had called TextFont.  So, I
gave up and did everything myself.  This is a demonstration of how to
create a dialog without using the Dialog Manager.
*/
 
#pragma segment Main
void DoAbout(void)
{
    Str255          verNum;
    Str255          appName;
    Ptr             aboutPtr;
    AboutWPeek      aboutPeek;
    ControlHandle   control;
    VersRecHndl     curVersion;
    OSErr           theErr;
    Boolean         ignore;
 
    KillSound();
    aboutPtr = NewPtrClear(sizeof(AboutWindow));
    if (aboutPtr != nil) {
        aboutPeek = (AboutWPeek)GetNewWindow(rAboutWindow, aboutPtr, (WindowPtr)-1);
        SetWRefCon((WindowPtr)aboutPeek, rAboutWindow);
        curVersion = (VersRecHndl)Get1Resource('vers', 1);
        if (curVersion != nil)
            PStringCopy((**curVersion).shortVersion, verNum); // get version string
        else
            verNum[0] = 0;                              // at least initialize it
        PStringCopy(LMGetCurApName(), appName);
        aboutPeek->appPict = Get1Resource('PICT', rAppPict);
        aboutPeek->comment = Get1Resource('STR ', rAboutText);
        control = GetNewControl(rAboutOkCntl, (WindowPtr)aboutPeek);
 
        if (    (aboutPeek->appPict != nil)
             && (aboutPeek->comment != nil)
             && (control != nil))
        {
            HNoPurge(aboutPeek->appPict);               // must keep them around
            HNoPurge(aboutPeek->comment);
            MyParamText((StringHandle)aboutPeek->comment, appName, verNum);
            if (!FailLowMemory(0))
                theErr = AsynchSndPlay((SndListHandle)Get1Resource(soundListRsrc, rMoofSound));
        }
        else                                            // couldnÕt build window
            ignore = DoCloseWindow((WindowPtr)aboutPeek);
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Get the selected sound and pass it to AsynchSndPlay or HyperSndPlay.  This
depends on the normal flag.  HyperCardÕs method is not normal.  This is
the HyperCard version of playing a sound.  It resamples any sound to
middle C.  If the resource is too large to fit in memory, IÕll get a nil
handle error from the SoundUnit.  It might be nice to test for this
before calling the SoundUnit and telling the user theyÕre too low on
memory, but the SoundUnit is robust enough and reports the error.  ItÕs
important to call KillSound to dispose of any data that was
allocated if an error were to occur.
*/
 
#pragma segment Main
void PlaySelectedSnd(SndDocPeek sndDoc, short message)
{
    SndListHandle   sndHandle;
    OSErr           theErr;
 
    theErr = GetSelection(sndDoc, &sndHandle);
    if (theErr == noErr) {
        if (message == sPlayingMsg)
            theErr = AsynchSndPlay(sndHandle);
        else
            theErr = HyperSndPlay(sndHandle);
    }
    if (theErr == noErr) {
        sndDoc->sndInUse = true;                // this document has a snd in use
        ShowStatusWindow(message);
    }
    else {
        KillSound();
        AlertUser(theErr, sSoundErr);
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Given a sound resource ID, this will get my song resource and call the
SoundUnit to use that sampled sound and song to play a tune.  This
application will play one of two songs for sampled sounds.  If any error
is encountered, I call KillSound to dispose of all data.
*/
 
#pragma segment Main
void PlaySndSong(SndDocPeek sndDoc, short sndID)
{
    SndChannelPtr   chan;
    SndListHandle   sndInst;
    SndListHandle   sndSong;
    OSErr           theErr;
 
    theErr = GetSelection(sndDoc, &sndInst);
    if (theErr == noErr) {
        sndSong = (SndListHandle)Get1Resource(soundListRsrc, sndID);
        theErr = ResError();                        // save any error
        if (sndSong != nil) {
            theErr = GetSampleChan(&chan, kInitNone, sndInst);
            if (theErr == noErr) {
                sndDoc->sndInUse = true;            // this document has a snd in use
                theErr = PlaySong(chan, sndSong);
            }
        }
    }
    if (theErr == noErr) {
        if (sndID == rScaleSnd)
            ShowStatusWindow(sScaleMsg);
        else                                        // I can play scales or a melody
            ShowStatusWindow(sMelodyMsg);
    }
    else {                                          // catch any errors
        KillSound();
        AlertUser(theErr, sSoundErr);
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Given a sound resource ID, this will get my song resource and call the
SoundUnit to use the note synthesizer and song to play a tune.  I request
the note channelÕs timbre (sounds like Òtom burrÓ).  If any error is
encountered, I call KillSound to dispose of all data.
*/
 
#pragma segment Main
void PlaySquareSong(short sndID)
{
 
    SndListHandle   sndSong;
    SndChannelPtr   chan;
    OSErr           theErr;
 
    sndSong = (SndListHandle)Get1Resource(soundListRsrc, sndID);
    if (sndSong != nil) {
        theErr = GetSquareWaveChan(&chan, kPreferredTimbre);
        if (theErr == noErr) {
            theErr = PlaySong(chan, sndSong);
            if (theErr == noErr) {
                if (sndID == rScaleSnd)
                    ShowStatusWindow(sScaleMsg);
                else                                // I play scales or a melody
                    ShowStatusWindow(sMelodyMsg);
            }
        }
        if (theErr != noErr) {                      // catch any errors
            KillSound();
            AlertUser(theErr, sSoundErr);
        }
    }
    else
        AlertUser(ResError(), sResErr);             // IÕll return the resource error
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This is a demonstration of the note synthÕs tone qualities (or the lack
there of).  This simply loops through timbres sending alternating note and
rest commands.  Once all the notes have been sent, then I need to send a
callBackCmd to signal the SoundUnit to dispose of the channel.  Well,
actually the SoundUnit will set a global flag that the application will be
polling for later in the event loop.  Once this happens, or if any errors
are encountered along the way, KillSound will be called.  One very
disappointing aspect to changing the timbre is that the Mac Plus or SE
cannot handle this while a note is sounding.  The Apple Sound Chip can and
if you wanted to remove the rests try this routine on a Mac II, for
example, you can hear a continuous note while the timbre changes.  Try
this on a Mac Plus and youÕll hear garbage.  IÕd show this myself, but how
do I determine if the Mac has the ASC?
 
BUG NOTE: There is problem when the final sound command is a freqDurationCmd.
The note will continue to sound, looping forever, until a quietCmd is sent
or the channel is disposed of.  To prevent unwanted looping, I send a
quietCmd after all notes.
*/
 
#pragma segment Main
void PlaySquareTimbres(void)
{
    SndChannelPtr   chan;
    short           timbre;
    OSErr           theErr;
 
    theErr = GetSquareWaveChan(&chan, kPreferredTimbre);
    if (theErr == noErr) {
        ShowStatusWindow(sTimbresMsg);
        for (timbre = kSineWave; timbre <= kSquareWave; timbre += 8) {
            theErr = SetSquareWaveTimbre(chan, timbre, kWait);
            if (theErr == noErr)
                theErr = SendNote(chan, kOneSecond / 2, kOctave7 + Akey);
            if (theErr == noErr)
                theErr = SendRest(chan, kOneSecond / 10);
            timbre = timbre + 8;                    // skip a few more timbres
            if (theErr != noErr)                    // if there was an error...
                break;                              // get out of the loop
        }
    }
    if (theErr == noErr)
        theErr = SoundComplete(chan);
    else {                                          // catch any errors
        KillSound();
        AlertUser(theErr, sSoundErr);
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This demonstrates the wave table synthesizers.  First thing to do is
obtain a wave cycle and the 'snd ' resource containing a song to be
played.  Then IÕm ready to get the four wave channels and install the
chosen wave.  Once the wave is installed into the channel I can dispose of
the memory used to create the wave, since the Sound Manager will copy it
to its internal buffers.  At this point IÕm ready to play the song.  ItÕs
not easy to hear four wave synths playing the same note with the same wave
table.  I was going to add a modifier to the channel that would transpose
each channels note, but I found a bug.
 
BUG NOTE: Installing a modifier to one of the wave table channels caused
the channel to fail.  My normal sequence of events is this: I synchronize
the four channels, send all the note commands, end this with the
callBackCmd, and finally release the channels to play their queues.  When
I added a modifier, the callBackCmd was the first command to be processed.
This caused my completion routines to be called and before the channel
made any sound it was disposed.  I tried this without the callBackCmd and
the channel never processed any commands.
 
BUG NOTE: The wave table synthesizer is not working on a non-Apple Sound
chip based Mac.  At least not so far with any System 6.0x releases.  This
leaves the Mac Plus/SE without four tone polyphonic sound.
 
VERSION 1.2: No longer need to call HasWorkingWaveTables since Sound Manager
2 and later fixed the problem.
*/
 
#pragma segment Main
void PlayWaveScale(void)
{
    SndChannelPtr   chan1, chan2, chan3, chan4;
    SndListHandle   sndSong;
    SndListHandle   waveHandle;
    Ptr             waveTablePtr;
    long            offSet;
    short           sndType;
    short           waveLength;
    OSErr           theErr;
 
    sndSong = (SndListHandle)Get1Resource(soundListRsrc, rScaleSnd);
    if (sndSong != nil) {
        waveHandle = (SndListHandle)Get1Resource(soundListRsrc, rTenorVox);
        theErr = HoldSnd(waveHandle);
        if (theErr == noErr) {
            offSet = GetSndDataOffset(waveHandle, &sndType, &waveLength);
            waveTablePtr = (Ptr)((long)*waveHandle + offSet);
            theErr = GetWaveChans(&chan1, &chan2, &chan3, &chan4);
            if (theErr == noErr) {
                theErr = InstallWave(chan1, waveTablePtr, waveLength);
                if (theErr == noErr) {
                    theErr = InstallWave(chan2, waveTablePtr, waveLength);
                    if (theErr == noErr) {
                        theErr = InstallWave(chan3, waveTablePtr, waveLength);
                        if (theErr == noErr)
                            theErr = InstallWave(chan4, waveTablePtr, waveLength);
                    }
                }
            }
            HUnlock((Handle)waveHandle);
            HPurge((Handle)waveHandle);
            if (theErr == noErr)
                theErr = Play4ChanSongs(chan1, chan2, chan3, chan4,
                                        sndSong, sndSong, sndSong, sndSong);
            if (theErr == noErr)
                ShowStatusWindow(sScaleMsg);
            else {                              // catch any SoundUnit errors
                KillSound();
                AlertUser(theErr, sSoundErr);
            }
        }
        else
            AlertUser(theErr, sResErr);         // couldnÕt get waveHandle
    }
    else
        AlertUser(ResError(), sResErr);         // couldnÕt get song
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This is a utility routine used to obtain the four snd resources that
contain parts of a song.  Nothing much to do other then get the resource
and do error checking.
*/
 
#pragma segment Main
OSErr GetSndSongs(short sndSongID1, short sndSongID2,
                    short sndSongID3, short sndSongID4,
                    SndListHandle *sndSong1, SndListHandle *sndSong2,
                    SndListHandle *sndSong3, SndListHandle *sndSong4)
{
    OSErr   result = noErr;                         // initialize result
 
    // get all the snds
    *sndSong1 = (SndListHandle)Get1Resource(soundListRsrc, sndSongID1);
    if (sndSong1 != nil) {
        *sndSong2 = (SndListHandle)Get1Resource(soundListRsrc, sndSongID2);
        if (sndSong2 != nil) {
            *sndSong3 = (SndListHandle)Get1Resource(soundListRsrc, sndSongID3);
            if (sndSong3 != nil)
                *sndSong4 = (SndListHandle)Get1Resource(soundListRsrc, sndSongID4);
        }
    }
    if ((sndSong1 == nil) || (sndSong2 == nil)
     || (sndSong3 == nil) || (sndSong4 == nil))
        result = ResError();                        // return resource error
    return (result);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This routine is a utility IÕm using to prepare four wave table channels.
First thing to do is obtain a wave table data I stored in a set of 'snd '
resources.  Then IÕm ready to install these wave tables into the four wave
channels.  Once the waves are installed into the channel I can dispose of
the memory used to create each wave, since the Sound Manager will copy
them to its internal buffers.  At this point the channels are ready to
play sounds.
*/
 
#pragma segment Main
OSErr InstallWaveSnds(SndChannelPtr chan1, SndChannelPtr chan2,
                      SndChannelPtr chan3, SndChannelPtr chan4,
                      short waveID1, short waveID2, short waveID3, short waveID4)
{
    SndListHandle   waveSnd1, waveSnd2, waveSnd3, waveSnd4;
    Ptr             wavePtr;
    long            offSet;
    short           waveLgth;
    short           sndType;
    OSErr           theErr;
 
    // get em and hold em down
    waveSnd1 = (SndListHandle)Get1Resource(soundListRsrc, waveID1);
    theErr = HoldSnd(waveSnd1);
    if (theErr == noErr) {
        waveSnd2 = (SndListHandle)Get1Resource(soundListRsrc, waveID2);
        theErr = HoldSnd(waveSnd2);
        if (theErr == noErr) {
            waveSnd3 = (SndListHandle)Get1Resource(soundListRsrc, waveID3);
            theErr = HoldSnd(waveSnd3);
            if (theErr == noErr) {
                waveSnd4 = (SndListHandle)Get1Resource(soundListRsrc, waveID4);
                theErr = HoldSnd(waveSnd4);
            }
        }
    }
    if (theErr != noErr)
        return (theErr);                            // weÕre out of here
                                                    // catch the waves
    offSet = GetSndDataOffset(waveSnd1, &sndType, &waveLgth);
    wavePtr = (Ptr)((long)*waveSnd1 + offSet);
    theErr = InstallWave(chan1, wavePtr, waveLgth);
    if (theErr == noErr) {
        offSet = GetSndDataOffset(waveSnd2, &sndType, &waveLgth);
        wavePtr = (Ptr)((long)*waveSnd1 + offSet);
        theErr = InstallWave(chan2, wavePtr, waveLgth);
        if (theErr == noErr) {
            offSet = GetSndDataOffset(waveSnd3, &sndType, &waveLgth);
            wavePtr = (Ptr)((long)*waveSnd1 + offSet);
            theErr = InstallWave(chan3, wavePtr, waveLgth);
            if (theErr == noErr) {
                offSet = GetSndDataOffset(waveSnd4, &sndType, &waveLgth);
                wavePtr = (Ptr)((long)*waveSnd1 + offSet);
                theErr = InstallWave(chan4, wavePtr, waveLgth);
            }
        }
    }
    HUnlock((Handle)waveSnd1);                      // done with resources
    HPurge((Handle)waveSnd1);
    HUnlock((Handle)waveSnd2);
    HPurge((Handle)waveSnd2);
    HUnlock((Handle)waveSnd3);
    HPurge((Handle)waveSnd3);
    HUnlock((Handle)waveSnd4);
    HPurge((Handle)waveSnd4);
    return (theErr);                                // return the error
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This is the demonstration of the wave table synthesizers.  This will get
four snd resources that contain parts to a song.  Then I get the four wave
table channels.  With these four channels, I install two other snd
resources that contain wave table data.  Once the four songs, four
channels, and two wave tables are ready then I play the song.
 
BUG NOTE: The wave table synthesizer is not working on a non-Apple Sound
chip based Mac.  At least not so far with any System 6.0x release.  This
leaves the Mac Plus/SE without four tone polyphonic sound.
 
VERSION 1.2: No longer need to call HasWorkingWaveTables since Sound Manager
2 and later fixed the problem.
*/
 
#pragma segment Main
void PlayWaveMelody(void)
{
    SndListHandle   sndSong1, sndSong2, sndSong3, sndSong4;
    SndChannelPtr   chan1, chan2, chan3, chan4;
    OSErr           theErr;
 
    theErr = GetSndSongs(rMelodyPart1, rMelodyPart2, rMelodyPart3, rMelodyPart4,
                                &sndSong1, &sndSong2, &sndSong3, &sndSong4);
    if (theErr != noErr) {
        AlertUser(theErr, sResErr);             // return the error
        return;                                 // weÕre out of here
    }
    theErr = GetWaveChans(&chan1, &chan2, &chan3, &chan4);
    if (theErr == noErr) {
        theErr = InstallWaveSnds(chan1, chan2, chan3, chan4,
                            rWaveMelody, rWaveHarmony, rWaveHarmony, rWaveHarmony);
        if (theErr == noErr)
            theErr = Play4ChanSongs(chan1, chan2, chan3, chan4,
                                        sndSong1, sndSong2, sndSong3, sndSong4);
    }
    if (theErr == noErr)
        ShowStatusWindow(sMelodyMsg);
    else {                                      // catch any errors
        KillSound();
        AlertUser(theErr, sSoundErr);
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This is the demonstration of the wave table synthesizers.  This will get
four snd resources that contain parts to a song.  Then I get the four wave
table channels.  With these four channels, I install four other snd resources
that contain wave table data.  Once the four songs, four channels, and
four wave tables are ready then I play the song.  By the way, SATB stands
for Soprano, Alto, Tenor, and Bass.  ItÕs standard music-speak.
 
BUG NOTE: The wave table synthesizer is not working on a non-Apple Sound
chip based Mac.  At least not so far with any System 6.0x release.  This
leaves the Mac Plus/SE without four tone polyphonic sound.
 
VERSION 1.2: No longer need to call HasWorkingWaveTables since Sound Manager
2 and later fixed the problem.
*/
 
#pragma segment Main
void PlayWaveSATB(void)
{
    SndListHandle   sndSong1, sndSong2, sndSong3, sndSong4;
    SndChannelPtr   chan1, chan2, chan3, chan4;
    OSErr           theErr;
 
    theErr = GetSndSongs(rCounterPt1, rCounterPt2, rCounterPt3, rCounterPt4,
                                &sndSong1, &sndSong2, &sndSong3, &sndSong4);
    if (theErr != noErr) {
        AlertUser(theErr, sResErr);                 // return the error
        return;                                     // weÕre out of here
    }
    theErr = GetWaveChans(&chan1, &chan2, &chan3, &chan4);
    if (theErr == noErr) {
        theErr = InstallWaveSnds(chan1, chan2, chan3, chan4,
                                    rSopranoVox, rAltoVox, rTenorVox, rBassVox);
        if (theErr == noErr)
            theErr = Play4ChanSongs(chan1, chan2, chan3, chan4,
                                        sndSong1, sndSong2, sndSong3, sndSong4);
    }
    if (theErr == noErr)
        ShowStatusWindow(sCounterPtMsg);
    else {                                          // catch any errors
        KillSound();
        AlertUser(theErr, sSoundErr);
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
VERSION 1.2: Create four sample sound channels with an instrument sound
installed in each one. Then play the four part song to demonstrate the
sample sound channel.
*/
 
#pragma segment Main
void PlaySampleSATB(void)
{
    SndListHandle   sndSong1, sndSong2, sndSong3, sndSong4;
    SndListHandle   sampleHarmony;
    SndChannelPtr   chan1, chan2, chan3, chan4;
    OSErr           theErr;
 
    theErr = GetSndSongs(rCounterPt1, rCounterPt2, rCounterPt3, rCounterPt4,
                                &sndSong1, &sndSong2, &sndSong3, &sndSong4);
    if (theErr != noErr) {
        AlertUser(theErr, sResErr);                 // return the error
        return;                                     // weÕre out of here
    }
 
    sampleHarmony = (SndListHandle)Get1Resource(soundListRsrc, rSampleHarmony);
    theErr = Get4SampleInstruments(&chan1, &chan2, &chan3, &chan4,
                            sampleHarmony, sampleHarmony, sampleHarmony, sampleHarmony);
    if (theErr == noErr)
        theErr = Play4ChanSongs(chan1, chan2, chan3, chan4,
                                sndSong1, sndSong2, sndSong3, sndSong4);
    if (theErr == noErr)
        ShowStatusWindow(sCounterPtMsg);
    else                                            // catch any errors
    {
        KillSound();
        AlertUser(theErr, sSoundErr);
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
VERSION 1.2: Create four sample sound channels with an instrument sound
installed in each one. Then play the four part song to demonstrate the
sample sound channel.
*/
 
#pragma segment Main
void PlaySampleMelody(void)
{
    SndListHandle   sndSong1, sndSong2, sndSong3, sndSong4;
    SndListHandle   sampleHarmony, sampleMelody;
    SndChannelPtr   chan1, chan2, chan3, chan4;
    OSErr           theErr;
 
    theErr = GetSndSongs(rMelodyPart1, rMelodyPart2, rMelodyPart3, rMelodyPart4,
                                &sndSong1, &sndSong2, &sndSong3, &sndSong4);
    if (theErr != noErr) {
        AlertUser(theErr, sResErr);                 // return the error
        return;                                     // weÕre out of here
    }
 
    sampleHarmony = (SndListHandle)Get1Resource(soundListRsrc, rSampleHarmony);
    sampleMelody = (SndListHandle)Get1Resource(soundListRsrc, rSampleMelody);
    theErr = Get4SampleInstruments(&chan1, &chan2, &chan3, &chan4,
                            sampleMelody, sampleHarmony, sampleHarmony, sampleHarmony);
    if (theErr == noErr)
        theErr = Play4ChanSongs(chan1, chan2, chan3, chan4,
                                sndSong1, sndSong2, sndSong3, sndSong4);
    if (theErr == noErr)
        ShowStatusWindow(sMelodyMsg);
    else                                            // catch any errors
    {
        KillSound();
        AlertUser(theErr, sSoundErr);
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
VERSION 1.1:  This is just one of the reasons I hate the Dialog Manager.
I needed a simple dialog to ask the user for a name for their new sound.
To do this, I only needed a simple dialog with two buttons and an
editable text item.  Since the ModalDialog will not draw an outline
around the default item, I then needed an userItem.  This is the really
stupid part. The userItem is only there just to get a chance to draw an
outline around the default button.  It has no other purpose.  It is not
visible, not does it get any user interaction what so ever.  It's just a
pain in the ass.
*/
 
#pragma segment Main
pascal void DefaultOutline(WindowPtr window, short theItem)
{
#pragma unused (theItem)
    Handle itemHndl;
    Rect itemRect;
    short kind;
 
    GetDialogItem((DialogPtr)window, ok, &kind, &itemHndl, &itemRect);
    DoButtonOutline((ControlHandle)itemHndl);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
VERSION 1.1:  Show a dialog asking the user to name their new sound
resource.  I need to outline the default button, so I have to install an
userItem setting its drawing procedure to do the outline around a
different item altogether.  The Dialog Manager really bugs me.
*/
 
#pragma segment Main
void GetSndName(Str255 sndName)
{
    DialogPtr dialog;
    Handle itemHndl;
    Rect itemRect;
    short kind;
    short theItem;
 
    dialog = GetNewDialog(rGetNameDLOG, nil, (WindowPtr)-1);
    GetDialogItem(dialog, rUserItem, &kind, &itemHndl, &itemRect);
    SetDialogItem(dialog, rUserItem, kind, (Handle)DefaultOutline, &itemRect);
    do
        ModalDialog(nil, &theItem);
    while ((theItem != ok) && (theItem != cancel));
    if (theItem == ok) {
        GetDialogItem(dialog, rNameItem, &kind, &itemHndl, &itemRect);
        GetDialogItemText(itemHndl, sndName);
    } else
        sndName[0] = 0;
    DisposeDialog(dialog);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
VERSION 1.1:  This routine adds the given snd resource to the file and
document. First thing to do is find a proper resource ID for the 'snd '.
There is a reserved range for them, didn't you read the documentation?
Once this new resource is added then I update the file and re-build the
list of sounds.  This is necessary since the list must be in the same
order as the sounds are in the file.  I ignore the error returned by
InitSndList, since the chances of it failing at this point are slim.  If
anything is wrong with the list, then the rest of this application is
robust enough to handle a resource problem.  The user would have to close
the document and attempt to open it again.  If adding the resources
works, but updating the file fails then I will remove it.  This keeps the
document consistent with the file.
*/
 
#pragma segment Main
OSErr AddSnd(SndDocPeek sndDoc, StringPtr sndNamePtr, SndListHandle sndHndl)
{
    short resID;
    OSErr theErr;
    short ignore;
 
    UseResFile(sndDoc->resFile);                //put our resource in the right file
    do
        resID = Unique1ID(soundListRsrc);
    while (resID < kSystemSndRange);
    AddResource((Handle)sndHndl, soundListRsrc, resID, sndNamePtr);
    theErr = ResError();
    UseResFile(gAppResRef);                     //restore our resource file
    if (theErr == noErr) {
        UpdateResFile(sndDoc->resFile);         //update the file
        theErr = ResError();
        if (theErr == noErr) {
            ignore = FlushVol(nil, sndDoc->vRefNum);
            LDelRow(0, 0, sndDoc->list);        //delete all the rows
            ignore = InitSndList(sndDoc);       //re-create the list
            SelectSndCell(sndDoc, resID);       //select the new snd
        } else
            RemoveResource((Handle)sndHndl);    //couldn't add it, update failed
    }
    return(theErr);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
VERSION 1.1:  This routine has one tricky aspect involving the Resource
Manager and removing a resource.  There's a catch.  I want the document
to always reflect the contents of the file on disk.  In other words,
InitSndList depends on UpdateResFile returning noErr.  If the file is
locked, then removing a resource shouldn't be allowed but RmveResource
will return noErr even if the file cannot be updated.  This is a
"feature" since the user may unlock the volume after removing the
resource and then UpdateResFile would work.  If RmveResource succeeds,
but UpdateResFile fails then the resource map in memory will be
inconsistent with what's on disk.  If the user then closed the document
and opened it again, the resource they just removed would magically still
be there.  The basic problem is that it is difficult to determine if a
given file will really allow write access.  There's at least three
situations to check: a locked file, a locked volume, or an AppleShare
folder privileges issue.  The ideal, and probably the best, solution
would be to deselect the edit commands from the user for a read-only
file.  This would involve lots of File Manager calls every time the user
selects a menu command.  (If you thought that GetFCBInfo would tell you
this, you're wrong.  You can have write permission to a file that is on a
locked volume.)  Additionally, the user needs to see that the reason the
edit menu doesn't work is because the file is read-only which would
require another feature to be added with lots more code.  This is why I
call ChangedResource before any other Resource Manager calls.
ChangedResource will determine if the resource can be written to disk.
If not, then it returns an error which is exactly what I wanted to know
in the first place.  I want to flush the cache to keep the disk's
resource map consistent with the resource data.  Otherwise, a crash could
occur the resource fork might be damaged and the file has to be repaired
(if possible) or deleted.  I'm ignoring the InitSndList result.  At this
stage, there's only a slim chance the list couldn't be re-built.  If it
does fail, the file should be closed.
 
A tip to the reader: dispose of as much memory as possible before calling
UpdateResFile.  This makes it faster.  UpdateResFile will not purge any
memory, but only attempts to use the available space.  If there's little
free space then UpdateResFile will run really slow.
*/
 
#pragma segment Main
void ClearSnd(SndDocPeek sndDoc)
{
    SndListHandle   sndHndl;
    OSErr           theErr;
    short           ignore;
 
    theErr = GetSelection(sndDoc, &sndHndl);            //get the resource to remove
    if (theErr == noErr) {
        ChangedResource((Handle)sndHndl);               //can we change the file?
        theErr = ResError();                            //save the error result
        if (theErr == noErr) {
            UseResFile(sndDoc->resFile);                //use the right resource file
            RemoveResource((Handle)sndHndl);            //remove that sucker
            theErr = ResError();                        //save the error result
            UseResFile(gAppResRef);                     //restore our resource file
            if (theErr == noErr) {
                DisposeHandle((Handle)sndHndl);         //get rid of the memory
                UpdateResFile(sndDoc->resFile);         //update the file
                theErr = ResError();                    //save the error result
                if (theErr == noErr)
                    ignore = FlushVol(nil, sndDoc->vRefNum);
                LDelRow(0, 0, sndDoc->list);            //delete all the rows
                ignore = InitSndList(sndDoc);           //re-create the list
                SelectNextCell(sndDoc->list, true);     //select the first cell
                ActivateSndCntls(sndDoc);               //may not be any sounds left
            }
        }
    }
    if (theErr != noErr)
        AlertUser(theErr, sEditErr);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
VERSION 1.1:  This routine will create a handle and record sound into it.
A new sound will prompt the user for a name to add it to the document.
The Sound Input Manager will re-size this handle to be as small as
possible to contain only the samples recorded.  You can pass sndHandl ==
nil to SndRecord, which will cause the Sound Input Manager to create a
handle for you.  If adding the new sound is successful, the new resource
handle is marked purgeable, because I only use them temporally and they
tend to be large.  I have to set the sndHandle variable to nil in this
case since it is now belongs to the Resource Manager and I don't want to
dispose of it in the error handling code.  The choice of recording
quality wasn't given to the user.  Typically, users would only be
confused by the question of "what compression ratio do you prefer?"  As
a power user option, it would be nice to let the user set the rate.  Be
careful since this is a user interface issue, and Apple expects to see
lots of Sound Input features in applications.
*/
 
#pragma segment Main
void DoRecordSound(SndDocPeek sndDoc)
{
    Str255          sndName;
    Point           recTopLeft;
    long            total;
    long            contig;
    SndListHandle   sndHandle;
    OSErr           theErr;
 
    KillSound();
    PurgeSpace(&total, &contig);
    sndHandle = (SndListHandle)NewHandle(contig - kMinSpace);
    if (sndHandle != nil) {
        recTopLeft.v = kRecordTop;
        recTopLeft.h = kRecordLeft;
        theErr = SndRecord(nil, recTopLeft, siBestQuality, &sndHandle);
        if (theErr == noErr) {
            GetSndName(sndName);
            theErr = AddSnd(sndDoc, sndName, sndHandle);
            if (theErr == noErr) {
                HPurge((Handle)sndHandle);
                sndHandle = nil;
            }
        }
    } else
        theErr = MemError();
    if (sndHandle != nil)
        DisposeHandle((Handle)sndHandle);
    if ((theErr != noErr) && (theErr != userCanceledErr))
        AlertUser(theErr, sSoundErr);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Enable and disable menus based on the current state.  The user can only
select enabled menu items so I set up all the menu items before calling
MenuSelect or MenuKey, since these are the only times that a menu item
can be selected. Note that MenuSelect is also the only time the user will
see menu items. This approach to deciding what enable/disable state a menu
item has the advantage of concentrating all the decision making in one
routine, as opposed to being spread throughout the application.  Other
application designs may take a different approach that may or may not be
just as valid.
*/
 
#pragma segment Main
void AdjustMenus(void)
{
    MenuHandle menu;
    WindowPtr window;
    Boolean allowEdit;
 
    window = FrontWindow();
    menu = GetMenuHandle(mFile);                    //the File menu and items
    if (IsDAWindow(window) || IsDocWindow(window))
        EnableItem(menu, iClose);
    else
        DisableItem(menu, iClose);
 
    menu = GetMenuHandle(mEdit);                    //the Edit menu and items
    allowEdit = IsDAWindow(window);
    if (allowEdit)                                  //check the undo item
        EnableItem(menu, iUndo);
    else
        DisableItem(menu, iUndo);
 
    if (IsDocWindow(window)) {                      //handle the other edit items
        allowEdit = HasSelection((SndDocPeek)window) || allowEdit;
        EnableItem(menu, iPaste);
    } else
        DisableItem(menu, iPaste);
 
    if (allowEdit) {
        EnableItem(menu, iCut);
        EnableItem(menu, iCopy);
        EnableItem(menu, iClear);
    } else {
        DisableItem(menu, iCut);
        DisableItem(menu, iCopy);
        DisableItem(menu, iClear);
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
VERSION 1.1:  Get the currently selected item and copy it to the
clipboard.  I want to also save the name for this resource, so I add a
string type to the clipboard as well.
*/
 
#pragma segment Main
void CopySnd(SndDocPeek sndDoc)
{
    Str255          sndName;
    ResType         rType;
    SndListHandle   sndHndl;
    long            scrapLen;
    short           id;
    OSErr           theErr;
 
    theErr = GetSelection(sndDoc, &sndHndl);
    if (theErr == noErr) {
        GetResInfo((Handle)sndHndl, &id, &rType, sndName);
        scrapLen = ZeroScrap();                     //ignoring the result
        theErr = PutScrap(StrLength(sndName) + 1, 'STR ', sndName);
        HLock((Handle)sndHndl);
        theErr = PutScrap(GetHandleSize((Handle)sndHndl), soundListRsrc, (Ptr)(*sndHndl));
        HUnlock((Handle)sndHndl);
        HPurge((Handle)sndHndl);
    }
    if (theErr != noErr)
        AlertUser(theErr, sEditErr);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
VERSION 1.1:  Copy the selection and then delete it.
*/
 
#pragma segment Main
void CutSnd(SndDocPeek sndDoc)
{
    CopySnd(sndDoc);
    ClearSnd(sndDoc);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
VERSION 1.1:  This routine will create a new resource into the file
containing the clipboard's sound data.  A name is attempted to be found
by first checking for the string type in the scrap.  This application
will always put both a string and a sound together on the clipboard.  But
if the user copied a sound from the Sound cdev, ResEdit, or by some other
method then I'll ask the user for a new name.  If adding the new resource
is successful, then I mark the resource as purgeable and set the sndHndl
variable to NIL.  The Resource Manager then owns the handle and I don't
want my error handling to dispose of this new handle.
*/
 
#pragma segment Main
void PasteSnd(SndDocPeek sndDoc)
{
    Str255          sndName;
    StringHandle    sndNameHndl;
    long            offset;
    long            scrapLen;
    SndListHandle   sndHndl;
    OSErr           theErr;
 
    sndHndl = (SndListHandle)NewHandle(0);
    sndNameHndl = NewString("\p");
    theErr = MemError();
    scrapLen = GetScrap((Handle)sndNameHndl, 'STR ', &offset);
    scrapLen = GetScrap((Handle)sndHndl, soundListRsrc, &offset);
    if (scrapLen > 0) {
        if (sndNameHndl != nil)
            PStringCopy(*sndNameHndl, sndName);
        else
            sndName[0] = 0;
        if (StrLength(sndName) == 0)
            GetSndName(sndName);
        theErr = AddSnd(sndDoc, sndName, sndHndl);
        if (theErr == noErr) {
            HPurge((Handle)sndHndl);
            sndHndl = nil;                                      //done with handle
            ActivateSndCntls(sndDoc);
        }
    } else
        theErr = scrapLen;
 
    if (sndHndl != nil)
        DisposeHandle((Handle)sndHndl);
    if (sndNameHndl != nil)
        DisposeHandle((Handle)sndNameHndl);
    if (theErr != noErr)
        AlertUser(theErr, sEditErr);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This is called when an item is chosen from the menu bar (after calling
MenuSelect or MenuKey). It performs the right operation for each command.
It is good to have both the result of MenuSelect and MenuKey go to
one routine like this to keep everything organized.
 
VERSION 1.1:  Supporting the edit commands for snd resources.
*/
 
#pragma segment Main
void DoMenuCommand(long menuResult)
{
    WindowPtr window;
    short menuID;
    short menuItem;                         //resource ID and item of the selected menu
    short daRefNum;
    Str255 daName;
    Boolean ignore;
 
    menuID = HighWord(menuResult);          //use macros for efficiency...
    menuItem = LowWord(menuResult);         //to get menu item number and menu number
    switch (menuID) {
 
        case mApple:
            switch (menuItem) {
                case iAbout:                //bring up alert for About
                    DoAbout();
                    break;
                default:                    //all non-About items in this menu are DAs
                    GetMenuItemText(GetMenuHandle(mApple), menuItem, daName);
                    daRefNum = OpenDeskAcc(daName);
                    break;
            }
            break;
 
        case mFile:
            switch (menuItem) {
                case iNew:
                    NewSoundDoc();
                    break;
                case iOpen:
                    GetSoundDoc();
                    break;
                case iClose:
                    ignore = DoCloseWindow(FrontWindow()); //I donÕt care if cancelled
                    break;
                case iQuit:
                    Terminate();
                    break;
            }
            break;
 
        case mEdit:                             //call SystemEdit for DA editing && MultiFinder
            if (! SystemEdit(menuItem - 1)) {   //since I donÕt do any editing
                window = FrontWindow();
                if (IsDocWindow(window)) {
                    switch (menuItem) {
 
                        case iCut:
                            CutSnd((SndDocPeek)window);
                            break;
                        case iCopy:
                            CopySnd((SndDocPeek)window);
                            break;
                        case iPaste:
                            PasteSnd((SndDocPeek)window);
                            break;
                        case iClear:
                            ClearSnd((SndDocPeek)window);
                            break;
                    }
                }
            }
            break;
 
        case mDemos:
            switch (menuItem) {
 
                case iCheckVolume:
                    CheckSoundVolume();
                    break;
 
                case iSquareScale:
                    PlaySquareSong(rScaleSnd);
                    break;
 
                case iSquareMelody:
                    PlaySquareSong(rMelodyPart1);
                    break;
 
                case iSquareTimbre:
                    PlaySquareTimbres();
                    break;
 
                case iWaveScale:
                    PlayWaveScale();
                    break;
 
                case iWaveMelody:
                    PlayWaveMelody();
                    break;
 
                case iWaveSATB:
                    PlayWaveSATB();
                    break;
 
                case iSampleMelody:
                    PlaySampleMelody();
                    break;
 
                case iSampleSATB:
                    PlaySampleSATB();
                    break;
 
            } //switch (menuItem)
 
    } //switch (menuID)
    HiliteMenu(0);                      //unhighlight what MenuSelect or MenuKey hilited
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Draw the contents of the application window in response to an update
event.  At this point, BeginUpdate has been called which sets the windowÕs
visRgn to clip drawing only where it needs to be done.  I have the
controls to draw, a list to update, and a default button to outline.  I
use UpdateControls to avoid needless drawing that happens with DrawControls.
It not only runs faster but doesnÕt flicker.
*/
 
#pragma segment Main
void DrawSndWindow(WindowPtr window)
{
    ControlHandle control;
    Rect theRect;
 
    PenNormal();
    LUpdate(window->visRgn, ((SndDocPeek)window)->list);    // update list
    theRect = (**((SndDocPeek)window)->list).rView;         // frame the list
    theRect.right = theRect.right + kScrollbarAdjust;
    InsetRect(&theRect, kListFrameInset, kListFrameInset);
    FrameRect(&theRect);
    UpdateControls(window, window->visRgn);                 // update controls
    control = (ControlHandle)((WindowPeek)window)->controlList;         // draw button outline
    while (control != nil) {
        if (GetControlReference(control) == rPlaySndCntl)
            DoButtonOutline(control);
        control = (**control).nextControl;
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
When the user types a key, I check if it is one that IÕm looking for.
This routine only handles the non-command key events.  Here IÕm looking
for a arrow key or the return and enter keys for the default button.
 
VERSION 1.1: The enter and return keys only work if there is a selection.
Now supporting the backspace and delete keys which clear the selection.
*/
 
#pragma segment Main
void DoKeyDown(char key, WindowPtr window)
{
    ControlHandle control;
    Boolean ignore;
 
    switch (GetWRefCon(window)) {
 
        case rSoundWindow:
            switch (key) {
 
                case kEnterKey:
                case kReturnKey:
                    if (HasSelection((SndDocPeek)window)) {
                        control = (ControlHandle)((WindowPeek)window)->controlList;
                        while (control != nil) {            //find the default button
                            if (GetControlReference(control) == rPlaySndCntl)
                                SelectButton(control);          //here it is
                            control = (**control).nextControl;
                        }
                        PlaySelectedSnd((SndDocPeek)window, sPlayingMsg);
                    }
                    break;
 
                case kUpArrow:
                    SelectNextCell(((SndDocPeek)window)->list, false);
                    ActivateSndCntls((SndDocPeek)window);
                    break;
 
                case kDownArrow:
                    SelectNextCell(((SndDocPeek)window)->list, true);
                    ActivateSndCntls((SndDocPeek)window);
                    break;
 
                case kBackspace:
                    ClearSnd((SndDocPeek)window);
                    break;
            }
            break; //rSoundWindow
 
        case rStatusWindow:
            if (    (key == kEnterKey)
                 || (key == kReturnKey)
                 || (key == kEscape) ) {
                SelectButton((ControlHandle)((WindowPeek)window)->controlList);
                KillSound();
            }
            break;
 
        case rAboutWindow:
            if (    (key == kEnterKey)
                 || (key == kReturnKey)
                 || (key == kEscape) ) {
                SelectButton((ControlHandle)((WindowPeek)window)->controlList);
                ignore = DoCloseWindow(window);
            }
            break;
    } //switch (GetWRefCon(window))
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Given a document, this will handle any clicking in the list.  The mouse
point (event.where) is expected to have been adjusted to the local
coordinates of the window.  If this routine does handle the event, then
I return true.
 
BUG NOTE: LClick will return a double-click even if no cell was selected.
So I have to test for the last click being in a real cell.  If not, then
I will not process the double click.  Also, there is another bug in the
List Manager.  It will not always deselect the currently selected cell when
the user clicks outside of the data bounds.  In other words, sometimes
my list would show 4 items when the list has room to show 8.  If the user
clicked in the bottom area of the list (below the last item) the List
Manager should deselect any items.  It doesnÕt all the time, just sometimes.
The only real solution would be to write a new LClick.
*/
 
#pragma segment Main
Boolean ListClick(SndDocPeek sndDoc, EventRecord *event)
{
    Cell aCell;
    Rect listRect;
    Boolean result;
 
    listRect = (**(sndDoc->list)).rView;
    listRect.right = listRect.right + kScrollbarAdjust;
    if (PtInRect(event->where, &listRect)) {
        if (LClick(event->where, event->modifiers, sndDoc->list)) {
            aCell = LLastClick(sndDoc->list);
            if ( PtInRect(aCell, &((**(sndDoc->list)).dataBounds)) )
                PlaySelectedSnd(sndDoc, sPlayingMsg);
        }
        ActivateSndCntls(sndDoc);
        result = true;                                  //I handled the event
    } else
        result = false;
    return(result);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This is called when a mouse-down event occurs in the content of my
application windows.  First thing to check for is a click in the list.
If not a list click, then find what control may have been clicked in and
handle that.
 
VERSION 1.1: Handle the record button.
*/
 
#pragma segment Main
void DoSndDocClick(SndDocPeek sndDoc, EventRecord *event)
{
    ControlHandle control;
 
    SetPort((GrafPtr)sndDoc);
    GlobalToLocal(&event->where);
    if (! ListClick(sndDoc, event)) {
        if (FindControl(event->where, (WindowPtr)sndDoc, &control) != 0) {
            if (TrackControl(control, event->where, nil) != 0) {
                switch (GetControlReference(control)) {
 
                    case rPlaySndCntl:
                        PlaySelectedSnd(sndDoc, sPlayingMsg);
                        break;
 
                    case rHyperPlayCntl:
                        PlaySelectedSnd(sndDoc, sHyperMsg);
                        break;
 
                    case rPlayScaleCntl:
                        PlaySndSong(sndDoc, rScaleSnd);
                        break;
 
                    case rMelodyCntl:
                        PlaySndSong(sndDoc, rMelodyPart1);
                        break;
 
                    case rStopCntl:
                        KillSound();
                        break;
 
                    case rRecordCntl:
                        DoRecordSound(sndDoc);
                        break;
 
                } //switch GetControlReference(control)
            } //if TrackControl
        } //if FindControl
    } //if ! ListSelect
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
The status window has a stop button.  This will stop any sound in
progress.  So, if the user clicks in my status window I need to check
for this.
*/
 
#pragma segment Main
void DoStatClick(StatWindowPeek statWindow, EventRecord *event)
{
    ControlHandle control;
 
    SetPort((GrafPtr)statWindow);
    GlobalToLocal(&(event->where));
    if (FindControl(event->where, (WindowPtr)statWindow, &control) != 0)
        if (TrackControl(control, event->where, nil) != 0)
            KillSound();
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
The about window has a single OK button.  If the users clicks in it, then
close the window.
*/
 
#pragma segment Main
void DoAboutClick(WindowPtr window, EventRecord *event)
{
    ControlHandle control;
    Boolean ignore;
 
    SetPort(window);
    GlobalToLocal(&event->where);
    if (FindControl(event->where, (WindowPtr)window, &control) != 0) {
        if (TrackControl(control, event->where, nil) != 0)
            ignore = DoCloseWindow(window);
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This is called when an update event is received for any of my windows.  It
calls the appropriate windowÕs update routine to draw its contents.  As an
efficiency measure that does not have to be followed, it calls the drawing
routine only if the visRgn is non-empty.  This will handle situations
where calculations for drawing or drawing itself is very time-consuming.
Why does QD give you an update with an empty updateRgn?
*/
 
#pragma segment Main
void DoUpdate(WindowPtr window)
{
    BeginUpdate(window);                            //setup the visRgn, clears updateRgn
    if (! EmptyRgn(window->visRgn)) {               //if updating to be done
        SetPort(window);                            //set to the current port
        switch (GetWRefCon(window)) {               //call the windowÕs drawing routine
 
            case rSoundWindow:
                DrawSndWindow(window);
                break;
 
            case rStatusWindow:
                DrawStatusWindow();
                break;
 
            case rAboutWindow:
                DrawAboutWindow(window);
                break;
        }
    }
    EndUpdate(window);                              //restores the visRgn
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This is called when a window is to be activated or deactivated.  For the
document window I activate all the controls and the list.  For the status
window I activate the one and only control.  This is also called for
suspend and resume events while running MultiFinder.
*/
 
#pragma segment Main
void DoActivate(WindowPtr window, Boolean becomingActive)
{
    if (window != nil) {
        switch (GetWRefCon(window)) {
 
            case rSoundWindow:
                ActivateSndCntls((SndDocPeek)window);
                LActivate(becomingActive, ((SndDocPeek)window)->list);
                break;
 
            case rStatusWindow:
                if (becomingActive)                         //it only has one control
                    HiliteControl((ControlHandle)((WindowPeek)window)->controlList, kControlNoPart);
                else
                    HiliteControl((ControlHandle)((WindowPeek)window)->controlList, kControlInactivePart);
                DoButtonOutline((ControlHandle)((WindowPeek)window)->controlList);
                break;
 
            case rAboutWindow:
                if (becomingActive)                         //it only has one control
                    HiliteControl((ControlHandle)((WindowPeek)window)->controlList, kControlNoPart);
                else
                    HiliteControl((ControlHandle)((WindowPeek)window)->controlList, kControlInactivePart);
                DoButtonOutline((ControlHandle)((WindowPeek)window)->controlList);
                break;
        }
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
The required AppleEvent sent to tell an application to quit. Nothing much to do.
Just call our terminate routine to clean up and then exit through the event loop.
Note that you cannot exit from here, the system will crash. You must return to
the AppleEvent Manager. Do not call ExitToShell at this point!
*/
 
#pragma segment Main
pascal OSErr QuitApplicationEvent(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon)
{
#pragma unused (theAppleEvent, reply, handlerRefcon)
 
    Terminate();
    return(noErr);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
One of the required AppleEvents. This is a request to open a document. Find
the document being requested, and open it.
*/
 
#pragma segment Main
pascal OSErr OpenDocumentsEvent(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon)
{
#pragma unused (reply, handlerRefcon)
 
    FSSpec              file;
    AEDescList          docList;
    long                i;
    long                itemsInList;
    Size                actualSize;
    AEKeyword           keyword;
    DescType            returnedType;
    OSErr               theErr;
 
    // get the direct parameter--a descriptor list--and put it into docList
    theErr = AEGetParamDesc(theAppleEvent, keyDirectObject, typeAEList, &docList);
    if (theErr == noErr)
    {
        if (docList.descriptorType != typeAEList)
        {
            AEDisposeDesc (&docList);
            theErr = paramErr;
        }
        else
        {
            // count the number of descriptor records in the list
            theErr = AECountItems(&docList, &itemsInList);
            if (theErr == noErr)
            {
                //get each descriptor record from the list, coerce the returned data
                //to an FSSpec and open the associated file
 
                for (i = 1; i <= itemsInList; i++)
                {
                    theErr = AEGetNthPtr(&docList, i, typeFSS, &keyword, &returnedType,
                                            &file, sizeof(file), &actualSize);
                    if (theErr == noErr)
                        OpenSoundDoc(&file);
                }
            }
            AEDisposeDesc (&docList);
        }
    }
    return (theErr);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
It's a required AppleEvent and we don't use it.
*/
 
#pragma segment Main
pascal OSErr PrintDocumentsEvent(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon)
{
#pragma unused (theAppleEvent, reply, handlerRefcon)
 
    return (errAEEventNotHandled);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
It's a required AppleEvent and we don't use it.
*/
 
#pragma segment Main
pascal OSErr OpenApplicationEvent(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon)
{
#pragma unused (theAppleEvent, reply, handlerRefcon)
 
    return (noErr);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
As Spike Lee says, ÒDo the right thing.Ó Determine what kind of event it
is, and call the appropriate routines.  Key down events are first tested
as being command key events for menus or command-. for cancel.  If itÕs
the command-. the user is asking to cancel the sound.  In this case, if
the front window is the status window then the button should look as if it
has been clicked.  This is proper human interface.  Any non-menu command
keys are passed to DoKeyDown.  I have a global flag, gInModalState, which
is used to handle the front window as a modal dialog.  This means I ignore
clicks outside of the modal window and menu command keys.  I could have a
different global flag that would mean itÕs a modal window but uses menu
commands, such as gInModalMenuState.  An important note is that I do pass
any other non-menu command keys to DoKeys, so the modal window could have
access to key events.  One last thing to note happens during a suspend
event.   Applications using sound should dispose of their sound channels
at suspend time.  No other application can use sound while another one has
channels allocated.  To stop my sound, I call KillSound which hides the
status window.  MultiFinder does properly deactivate the front window at
suspend time and applications normally do not have to worry about
HiliteWindow.  I do because the front window could have been the status
window and IÕve removed it after getting a suspend event.  This causes the
next window, a document window, to become highlighted as the active
window.  To avoid my next window from being highlighted while in the
background, I have to call HiliteWindow *after* KillSound.  This is
strange and only applications changing the front window at suspend event
time have to be concerned about this.
 
VERSION 1.1: Added a constant, kSFTopLeft, to specify the dialog position.
I do not call KillSound during a MultiFinder switch unless we're running
the older Sound Manager.  The new one allows for multiple sound
channels or will return the proper error otherwise.
*/
 
#pragma segment Main
void DoEvent(EventRecord *event)
{
    WindowPtr window;
    Point where;
    short part;
    short err;
    Boolean ignore;
    char key;
 
    switch (event->what) {
 
        case mouseDown:
            part = FindWindow(event->where, &window);
            if (    (IsModalWindow(FrontWindow()) && (window != FrontWindow()))
                 || (IsModalWindow(FrontWindow()) && (part == inMenuBar)))
            {
                SysBeep(30);                            //click outside of modal window
                return;                                 //break out of routine
            }
            switch (part) {
                case inMenuBar:                             //process the menu command
                    AdjustMenus();
                    DoMenuCommand(MenuSelect(event->where));
                    break;
 
                case inSysWindow:                           //let system handle the mouseDown
                    SystemClick(event, window);
                    break;
 
                case inContent:
                    if ((window != FrontWindow()))
                        SelectWindow(window);
                    else {
                        switch (GetWRefCon(window)) {
 
                            case rSoundWindow:
                                DoSndDocClick((SndDocPeek)window, event);
                                break;
 
                            case rStatusWindow:
                                DoStatClick((StatWindowPeek)window, event);
                                break;
 
                            case rAboutWindow:
                                DoAboutClick(window, event);
                                break;
 
                            } //switch (GetWRefCon(window))
                        }
                    break; //inContent
 
                case inDrag:                        //pass screenBits.bounds to get all gDevices
                    DragWindow(window, event->where, &qd.screenBits.bounds);
                    break;
 
                case inGoAway:
                    if (TrackGoAway(window, event->where))
                        ignore = DoCloseWindow(window);
                    break;
 
            } //switch (part)
            break; //mouseDown
 
        case keyDown:
        case autoKey:
            window = FrontWindow();
            key = event->message & charCodeMask;
            if ((event->modifiers & cmdKey) != 0)           //Command key down?
            {
                if (key == kPeriod)
                {
                    if (window == (WindowPtr)gStatusWindow)
                        SelectButton((ControlHandle)((WindowPeek)gStatusWindow)->controlList);
                    KillSound();
                } else
                    if ((event->what == keyDown) && (! IsModalWindow(window)))
                    {
                        AdjustMenus();                      //adjust items properly
                        DoMenuCommand(MenuKey(key));
                    }
            } else                                          //non-Command keys
                if (window != nil)                          //if thereÕs a window
                    DoKeyDown(key, window);
            break;
 
        case activateEvt:                       //true for activate, false for deactivate
            DoActivate((WindowPtr)event->message, event->modifiers & activeFlag);
            break;
 
        case updateEvt:                         //call DoUpdate with the window to update
            DoUpdate((WindowPtr)event->message);
            break;
 
        case diskEvt:                               //Call DIBadMount in response to a diskEvt
            if (HighWord(event->message) != noErr) {
                where.v = kRecordTop;
                where.h = kRecordLeft;
                err = DIBadMount(where, event->message);
            }
            break;
 
        case osEvt:
            switch (event->message >> 24) {                 //get high byte of message
 
                case suspendResumeMessage:
                    if ((event->message & suspendResumeMessage) != 0)
                        gInBackground = false;              //it was a resume event
                    else {
                        gInBackground = true;               //it was a suspend event
                        if (GetSoundMgrVersion() == 1)
                            KillSound();                    //stop any sound
                        window = FrontWindow();             //get front window
                        if (window != nil)                  //donÕt use a nil window
                            HiliteWindow(window, false);    //then properly activate it
                    }
                    DoActivate(FrontWindow(), ! gInBackground);
                    break; //suspendResumeMessage
            }
            break; //osEvt
 
        case kHighLevelEvent:
            AEProcessAppleEvent(event);
            break;
 
    } //switch (event->what)
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Change the cursorÕs shape, depending on its current position.  This also
calculates the region where the current cursor resides (for
WaitNextEvent).  This is based on its current position in global
coordinates.  If the mouse is ever outside of that region, an event is
generated causing this routine to be called again by the event loop.  This
allows me to change the region to where the mouse is currently located.
If there is more to the event than just Òthe mouse moved,Ó this gets
called before the event is processed to make sure the cursor is the right
one.  In any (ahem) event, this is called again before I fall back into
WaitNextEvent.  Extreme short values are used to create a wide open
region.  -SHRT_MAX - 1 is the largest negative short (-32768) and SHRT_MAX
is the largest positive short (32767).
 
BUG NOTE: The largest positive value for a regionÕs size is SHRT_MAX - 1
due to a very old bug that still remains to this day.
*/
 
#pragma segment Main
void AdjustCursor(RgnHandle region)
{
    WindowPtr window;
    RgnHandle arrowRgn;
    RgnHandle sndCursorRgn;
    Rect sndCursorRect;
 
    window = FrontWindow();         //I only adjust the cursor when I am in front
    if ((! gInBackground) && (! IsDAWindow(window))) {
 
        arrowRgn = NewRgn();        //calculate regions for different cursor shapes
        sndCursorRgn = NewRgn();    //start with a big, big rectangular region
        SetRectRgn(arrowRgn, -SHRT_MAX - 1, -SHRT_MAX - 1, SHRT_MAX - 1, SHRT_MAX - 1);
 
        if (IsDocWindow(window)) {              //calculate region for document cursor
            sndCursorRect = window->portRect;
            SetPort(window);
 
            //make a global version of the viewRect
            LocalToGlobal(&TopLeft(sndCursorRect));
            LocalToGlobal(&BottomRight(sndCursorRect));
            RectRgn(sndCursorRgn, &sndCursorRect);
            SetOrigin(-window->portBits.bounds.left, -window->portBits.bounds.top);
            SectRgn(sndCursorRgn, window->visRgn, sndCursorRgn);
            SetOrigin(0, 0);
        }
 
        DiffRgn(arrowRgn, sndCursorRgn, arrowRgn);  //subtract other region
        if (PtInRgn(GetGlobalMouse(), sndCursorRgn)) {
            SetCursor(*(GetCursor(rSndCursor)));    //change cursor and region
            CopyRgn(sndCursorRgn, region);
        } else {
            SetCursor(&qd.arrow);
            CopyRgn(arrowRgn, region);
        }
        DisposeRgn(arrowRgn);                       //get rid of our local regions
        DisposeRgn(sndCursorRgn);
    }
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Get events forever, and handle them by calling DoEvent.  Since this
application requires System 6.0x or later I know that _WaitNextEvent is
always there, even without MultiFinder.  MultiFinderÕs sleep is used to
determine how often I want to receive events, regardless if an event has
actually occurred.  In this application, I donÕt perform any background
processing so IÕm being super friendly by using LONG_MAX as a sleep
value.  This also helps keep Virtual Memory from paging me in just to run
my event loop and find out that nothing happened.  The application will
only be called upon for events that must be handled.  Also call
AdjustCursor each time through the loop.  AdjustCursor will return the
current region containing the mouse and is passed to WaitNextEvent.  If
the mouse travels out of the region, another event is generated.  I have
to call AdjustCursor just before doing the event to make sure the right
cursor is shown.  Another thing, if I have a sound playing asynchronously
then I want to dispose of my channel as soon as possible.  I set up a flag
in the SoundUnit that will keep track of when it has a channel allocated.
I can call the HasChannelOpen() function to find out if this is true.  It
pretty much like keeping track of when youÕre in the background.  If a
channel is open, then the MultiFinder sleep time is adjusted to a
reasonable time that will allow me to catch when the sound has completed
so that I may dispose of my channels and status window.  ThatÕs when I
return kPollingSleepTime.
 
VERSION 1.1: No longer putting the SANELib into a seperate segment, which then
needed to be unloaded.  Instead, I merge it into the Main segment.  Refer
to the Make file for further information.
 
VERSION 1.2: No longer using the SANELib at all. All of the SANE calls
are inline, and do not need a library to be linked in.
*/
 
#pragma segment Main
void EventLoop(void)
{
    RgnHandle cursorRgn;
    EventRecord event;
    long sleep;
 
    cursorRgn = NewRgn();                   //1st time pass WNE an empty region
    while (!gTerminate) {
        if (HasChannelOpen())               //if weÕre playing a sound
            sleep = kPollingSleepTime;      //use the polling sleep value
        else {
            sleep = LONG_MAX;               //default value for sleep
            UnloadSeg(_SoundUnit);          //unload the Sound Unit
        }
        UnloadSeg(OpenSoundDoc);            //unload the open code
        if (HasSoundCompleted())
            KillSound();
        if (LowOnReserve())
            RecoverReserve();
        AdjustCursor(cursorRgn);            //get the right cursor
        if (WaitNextEvent(everyEvent, &event, sleep, cursorRgn)) {
            AdjustCursor(cursorRgn);        //get the right cursor
            DoEvent(&event);
        }
    };                                      //loop forever; I quit through Terminate
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
Set up the whole world.  Initialize global variables, Toolbox managers,
and menus.  If a failure occurs here, I will consider that the application
is in such bad shape that I should just exit.  Your error handling may
differ, but the checks should still be made.  I ask for a set of master
pointer blocks and all permanent storage at this point to cut down on
memory fragmentation.  I may be opening large numbers of resources and
documents so I allocate some extra master pointer blocks at the start.
 
VERSION 1.1:  Now supports opening of documents from the Finder.
*/
 
#pragma segment Initialize
void Initialize(void)
{
#define kBroughtToFront 3
 
    EventRecord event;
    Handle menuBar;
    long response;
    short count;
    OSErr ignoreErr;
    Boolean ignoreResult;
 
    gTerminate = false;
    gInBackground = false;                      //weÕll be in the foreground soon
    gAppResRef = CurResFile();                  //save the resRef to myself
    for (count = 1; count <= kNumberOfMasters; count++) //allocate master pointer blocks
        MoreMasters();
    InitGraf(&qd.thePort);                      //init managers, yawn...
    InitFonts();
    InitWindows();
    InitMenus();
    TEInit();
    InitDialogs(nil);
    InitCursor();
 
/*
ErrorSound is used to prevent the Dialog Manager from calling _SysBeep.
If I get a memory or resource Manager error it wouldnÕt be the best
of plans to call _SysBeep which will want to allocate memory and load
a few resources.  Which could be bad if I just gave an Alert signalling
an out of memory condition. You only have to install this for Sound Manager
version  1 which is pretty buggy.
 
VERSION 1.2: Since we're using Sound Manager 2 or later, we don't have to
disable the multiple sampled sound channels demo and we don't need DoErrorSound.
 
    if (GetSoundMgrVersion() == 1)
    {
        ErrorSound(GetRoutineAddress(DoErrorSound));
        DisableItem(GetMenu(mDemos), iSampleSATB);
        DisableItem(GetMenu(mDemos), iSampleMelody);
    }
*/
 
/*
This code is necessary to pull the application into the foreground.  I use
EventAvail because I donÕt want to remove any events the user may have
done, such as typing ahead.  Until the application has made a few calls (3
seems to be the magic number) to the Event Manager, MultiFinder keeps me
in the background.   Splashscreens and Alerts will remain in a background
layer until we get a few events.  This is documented in Tech Note #180.
*/
 
    for (count = 1; count <= kBroughtToFront; count++)
        ignoreResult = EventAvail(everyEvent, &event);
 
/*
Ignore the error returned from SysEnvirons; even if an error occurred,
the SysEnvirons glue will fill in the SysEnvRec. You can save a redundant
call to SysEnvirons by calling it after initializing AppleTalk.
 
VERSION 1.2: Using Gestalt and checking for System 7. Make sure you know if
the Gestalt trap is available before calling it. This code is compiled with the
SystemSevenOrLater flag on, when means you do not get the safe glue for Gestalt.
*/
    if (! TrapExists(_Gestalt))
        EmergencyExit(sWrongVersion);
 
    if (Gestalt(gestaltSystemVersion, &response) != noErr)
        EmergencyExit(sStandardErr);
 
    if (response < 0x0700)
        EmergencyExit(sWrongVersion);
 
/*
Call the SoundUnit to initialize itslef.  If the SoundUnit encounters an error,
then it cannot be used and this means IÕm leaving too.
*/
 
    if (InitSoundUnit() != noErr)               //allocates 4 * 1064 bytes
        EmergencyExit(sInitSoundErr);
 
/*
Before I go any further, I want my reserve memory.  This is an emergency
reserve (sorta like my old VW had) when memory runs low.  If I cannot
obtain this reserve, then IÕll bail.  ItÕs also important to obtain my
reserve before testing if I have the desired amount of memory to run
this application.  Also, FailLowMemory will consider the memory reserve.
*/
 
    if (! AllocateReserve())
        EmergencyExit(sLowMemory);
    SetGrowZone(GetRoutineAddress(MyGrowZone));
 
    menuBar = GetNewMBar(rMenuBar);                 //read menus into menu bar
    if (menuBar == nil)
        EmergencyExit(sNoMenus);                    //wow, howÕd that happen?
    SetMenuBar(menuBar);                            //install menus
    DisposeHandle(menuBar);
    AppendResMenu(GetMenuHandle(mApple), 'DRVR');   //add DA names to Apple menu
    DrawMenuBar();
 
    InitStatusWindow();                             //get the status window ready
    //InitCursorCtl(nil);                               //MPWÕs handy cursor routines
 
/*
Last, I want to make sure that enough memory is free for my application to
run.  It is possible that user may have adjusted the SIZE resource to too
small a setting or for some other reason the application started up in a
very small memory partition.  ItÕs also possible for a situation to arise
where the heap may have been of the requested size taken from the SIZE
resource, but a large scrap was loaded which left too little memory.  I
want to make sure that my free memory is not being modified by the scrapÕs
presence.  So, I unload it to disk but if the application will run once
the scrap is unloaded, then youÕll probably not get it back into memory.
Thus losing the clipboard contents.  I preform this check after
initializing all the Toolbox and the basic features of this application,
such as showing the about box.
*/
    if (FailLowMemory(kMinSpace)) {
        if (UnloadScrap() != noErr)
            EmergencyExit(sLowMemory);
        else {
            if (FailLowMemory(kMinSpace))
                EmergencyExit(sLowMemory);
        }
    }
 
/*
Install the four core AppleEvent handlers.
*/
    ignoreErr = AEInstallEventHandler(kCoreEventClass, kAEQuitApplication,
                GetRoutineAddress(QuitApplicationEvent), 0, false);
 
    ignoreErr = AEInstallEventHandler(kCoreEventClass, kAEOpenDocuments,
                GetRoutineAddress(OpenDocumentsEvent), 0, false);
 
    ignoreErr = AEInstallEventHandler(kCoreEventClass, kAEPrintDocuments,
                GetRoutineAddress(PrintDocumentsEvent), 0, false);
 
    ignoreErr = AEInstallEventHandler(kCoreEventClass, kAEOpenApplication,
                 GetRoutineAddress(OpenApplicationEvent), 0, false);
}
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
This routine is contained in the MPW runtime library.  It will be placed
into the code segment used to initialize the A5 globals.  This external
reference to it is done so that we can unload that segment, named %A5Init.
*/
 
#ifdef applec
extern void _DataInit(void);
#endif
 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MAIN PROGRAM ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
If you have stack requirements that differ from the default, then you
could use SetApplLimit to increase StackSpace at this point, before
calling MaxApplZone.
*/
 
#pragma segment Main
void main(void)
{
#ifdef applec
    UnloadSeg(_DataInit);           //note that _DataInit must not be in Main!
#endif
    MaxApplZone();                  //expand the heap so code segments load at the top
    Initialize();                   //initialize the program
    UnloadSeg(Initialize);          //note that Initialize must not be in Main!
    EventLoop();                    //call the main event loop
    ExitToShell();                  //we're out of here!
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MAIN PROGRAM ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~