December 94 - Make Your Own Sound Components
Make Your Own Sound Components
Kip Olson
Sound Manager 3.0, Apple's current audio software release, has an extensible
architecture based on sound components that makes it easy for developers to add
support for third-party audio hardware and any compressed audio format. Inside
Macintosh: Sound gives the theory of how to do this; this article illustrates how to
implement the theory, with examples of a sound output component and a sound
decompression component.
Since its release as a system extension in June 1993, Sound Manager 3.0 has offered
developers the possibility of obtaining high-quality digital audio output from the
Macintosh using third-party audio hardware and any compressed audio format. Sound
Manager 3.0 is now built into all shipping Macintosh computers and is fully integrated
into System 7.5. QuickTime 2.0 also takes advantage of new Sound Manager features to
provide an even higher level of audio support for multimedia applications.
"Somewhere in QuickTime: What's New With Sound Manager 3.0" in develop Issue 16
gives a brief sketch of Sound Manager 3.0 and the new vistas it open.
The Sound Manager architecture and the sound component programming interface are
described in detail in Inside Macintosh: Sound. What you won't find there is an
illustration of how Sound Manager features are implemented in practice. This article
fills that gap by offering two examples of sound components. On this issue's CD you'll
find NoiseMaker, a sound output component, and MewLaw, a sound decompression
component. After briefly describing how Sound Manager 3.0 works, I'll explain how to
make a sound output component and a sound decompression component, with reference
to these examples.
HOW SOUND MANAGER 3.0 WORKS
Sound Manager 3.0 uses an architecture based on sound components to process audio
samples for playback. A sound component is a software module that performs a specific
task, usually some kind of audio processing like decompression, rate conversion,
sample format conversion, or mixing. Sound components use the Component Manager
for registration, loading, and execution.
Figure 1 diagrams a typical sound playback scenario and the sound components that are
used. Basically, the sequence is as follows: When an application wants to produce a
sound, it calls the Sound Manager to open a sound channel and play the sound. In
response the Sound Manager creates a chain of sound components for this channel,
where each component performs a specific operation on the audio data. The Sound
Manager passes the audio data to the first component in the chain, which can be a
decompression component if the data is compressed, a format conversion component if
the sample size needs to be changed, or a rate conversion component if the sample rate
needs to be adjusted. These components can be applied in series to completely process
the audio data into the required format. The mixer component then sums all these audio
streams together and provides a single audio source for the sound output component,
which uses hardware to convert it to an audible sound.
Figure 1. A typical playback scenario
The sound output component (sometimes called the sound output device component or
the output device component) is a software module that identifies, controls, and plays
audio samples on some audio hardware device. This device can be a plug-in audio board,
a telecommunications pod, or just about anything else that can play sound. Apple
provides a sound output component for the built-in audio hardware on every Macintosh
except the Macintosh Plus, SE, and Classic machines.
All the sound output components installed on a Macintosh have icons displayed in the
Sound control panel that ships with Sound Manager 3.0. The user selects which sound
output component to play sounds with by clicking an icon. Figure 2 shows a situation
where two sound output components are available: the standard built-in Macintosh
sound output component and our example sound output component, NoiseMaker.
Figure 2. The Sound control panel
When the Sound Manager wants to play a sound, it opens the selected sound output
component and sends it commands to start playing the sound. The component is closed
when sound playback is completed. The sound output component is responsible for
opening a mixer component, which handles the complicated work of allocating chains of
sound components and processing the audio data. Our example sound output component,
NoiseMaker, shows how this works.
Apple provides sound components for mixing, rate conversion, sample format
conversion, and decompression. In addition, sound components can be defined to expand
compressed audio from any other format into a format that can be used by other
components and played by the hardware. Our example decompression component,
MewLaw, illustrates this.
MAKING A SOUND OUTPUT COMPONENT
Now I'll explain how to make your own sound output component, using NoiseMaker as
an example. NoiseMaker doesn't actually control an audio hardware device but rather
plays sounds using normal Sound Manager routines. It can be installed on any
Macintosh running Sound Manager 3.0 and is meant to be used as a template to manage
your own audio hardware.
Here's how NoiseMaker works: The NoiseMaker component is loaded at system startup
if the Register method called at that time indicates that the corresponding hardware is
available. When the Sound Manager plays a sound using NoiseMaker, it first calls the
Open method to open the component, followed by a call to the InitOutputDevice method
to have NoiseMaker do any hardware initializations and open a mixer component. It
then calls the PlaySourceBuffer method to start the sound playing. When the sound has
finished playing, the Sound Manager calls the Close method to have NoiseMaker release
the audio hardware and dispose of any memory it created.
I'll refer to NoiseMaker as I describe how to register a sound output component, the
structure of a sound output component, the dispatcher, and the methods and interrupt
routine that must be implemented.
REGISTRATION AND LOADING
In order for a sound component to be recognized by the Sound Manager, it must be
registered with the Component Manager. This is most easily done by creating a file of
type 'thng' containing a 'thng' resource that describes your sound component. When you
place this file in the Extensions folder, the Component Manager will automatically load
the sound component every time the Macintosh starts up. Listing 1 is a 'thng' resource
describing our example sound output component.
Listing 1. The 'thng' resource for NoiseMaker
#define cmpWantsRegisterMessage (1 << 31)
#define componentDoAutoVersion (1 << 0)
#define kNoiseMakerVersion 0x00010000
#define kNoiseMakerComponentID 128
resource 'thng' (kNoiseMakerComponentID, purgeable) {
'sdev', // sound output component type
'NOIS', // subtype of this component
'appl', // manufacturer
cmpWantsRegisterMessage, 0, // component flags
'proc', kNoiseMakerComponentID, // code resource
'STR ', kNoiseMakerComponentID, // component name
'STR ', kNoiseMakerComponentID+1, // component description
'ICON', kNoiseMakerComponentID, // component icon
kNoiseMakerVersion, // component version
componentDoAutoVersion, // registration flags
0, 0 // icon family ID, platform
};
For full details on the 'thng' resource, see the Component Manager
documentation in Inside Macintosh: More Macintosh Toolbox.*
The component type and subtype in the 'thng' resource identify the component so that
the Sound Manager can find it. The subtype must be unique for each hardware device
connected and must contain at least one uppercase character (Apple has dibs on
all-lowercase types); it's usually advisable to use an application creator type that has
been registered with Apple's Developer Support Center to avoid conflicts with other
companies. Similarly, the manufacturer name should identify your company and must
contain at least one uppercase character; in our example, Apple is the manufacturer so
we can get away with using all lowercase letters.
Setting the cmpWantsRegisterMessage bit in the component flags causes the Component
Manager to call the sound component with the Register method during the startup
process so that the component can determine whether its hardware is available (more
on this later). The code resource is the resource type and ID of the code that
implements your component. The component description is a string that describes the
function of the component to the user. The component name and component icon are
used in the Sound control panel, as shown in Figure 2.
The component version and the registration flags are used by the Component Manager
during loading to determine whether this component should replace an existing one. If
the componentDoAutoVersion bit is set in the registration flags, the Component
Manager will install this component only if the version given here is greater than for
any other existing component.
The icon family ID and platform fields aren't used by our component.
THE COMPONENT'S STRUCTURE
Sound output components use the standard format required by the Component Manager.
The main entry point is a dispatcher, which uses a selector to call the appropriate
subroutines (methods). The standard Component Manager methods must be supported,
along with a number of additional methods defined for sound output components. The
sound output component also contains an interrupt routine that functions as its
heartbeat.
Sound output components can create globals that are passed to each method. In addition,
there can be one set of global variables that the Component Manager maintains even
when the sound output component is closed, which is useful for storing state
information. (More about this later when I describe the InitOutputDevice method.)
THE DISPATCHER
The first routine in the component is the dispatcher, which uses a given selector to
call the appropriate method. Selectors used by the dispatcher have three ranges,
described in Table 1.
A number of methods are defined for sound components that don't need to be
implemented by every type of sound component. For example, the method
SoundComponentAddSource is used only by mixer components and shouldn't be
implemented by sound output components. When a component receives a selector that it
doesn't support but that can be delegated, it should delegate that selector to its source
component and let that component take care of it.
Listing 2 is the dispatcher from our example sound output component. This dispatcher
calls an internal utility routine called GetComponentRoutine that returns the address
of the routine to call based on the selector. If the sound output component doesn't
implement a method, it returns kDelegateComponentCall (-1) as the routine address,
which is a flag that this method should be delegated. If the routine returns nil, this is a
nondelegatable selector not supported by this component, and an error should be
returned. Otherwise, this is a valid method address and the method should be called.
Listing 2. The dispatcher from NoiseMaker
#define kDelegateComponentCall -1
pascal ComponentResult NoiseMaker(ComponentParameters *params,
GlobalsPtr globals)
{
ComponentRoutine theRtn;
ComponentResult result;
// Get address of component routine.
theRtn = GetComponentRoutine(params->what);
if (theRtn == nil)
// Selector isn't implemented.
result = badComponentSelector;
else if (theRtn == kDelegateComponentCall)
// Selector should be delegated.
result = DelegateComponentCall(params,
globals->sourceComponent);
else
// Call appropriate method.
result = CallComponentFunctionWithStorage((Handle) globals,
params, (ComponentFunctionUPP) theRtn);
return (result);
}
STANDARD COMPONENT MANAGER METHODS
The Component Manager requires that every sound component implement five standard
methods, as listed in Table 2. I'll describe each of these methods here; look at the
NoiseMaker code to see them in practice.
The Open method is the first method called when a component is opened. This method
must create the component globals and store them with the Component Manager, which
will then pass these globals to all subsequent methods. While this sounds fairly simple
to implement, there are a number of nuances that frequently escape the attention of
component writers and wreak havoc; read "Pitfalls of the Open Method" and be
forewarned!
______________________________
PITFALLS OF THE OPEN METHOD
Implementing the Open method can be straightforward if you watch out for
some common pitfalls. Most important, do not access or even look for your
hardware in the Open method! There is a separate method (InitOutputDevice)
for initializing hardware. The Open method should only allocate instance
globals and return. There are two reasons for this:
• If you access hardware in the Open method that isn't available, you
might crash or make bad assumptions. The possibility that you might
try to access hardware that isn't available is a real one because the
Component Manager calls the Open method before it calls the method
that checks to see whether your hardware is installed (the Register
method).
• Sometimes the component is opened when sound is already playing,
to get status information like sample rates and sizes. For instance, the
Sound control panel does this to display hardware settings to the user.
Resetting or changing hardware in any way during this kind of Open
component operation would obviously be disruptive to the sound.
Another tricky interaction with the Component Manager comes into play when
you're trying to decide where to create the component globals. Because the
sound output component is shared by all applications that are playing sound,
the Component Manager will attempt to load the component in the system heap.
In this case, your component should create its globals in the system heap as
well, so you aren't dependent on any application heaps.
However, if the Component Manager can't find enough space in the system heap
to load the component, it will load it in the application heap. In this case,
you'll want to create your globals in the application heap as well.
The Component Manager gives you a way to determine where you should create
your globals. The call GetComponentInstanceA5 returns the A5 world for the
component. If it returns 0, the component was loaded in the system heap and
the globals should go there as well; otherwise, the component is in the
application heap and the globals should also be created there. The NoiseMaker
code shows how this works. The Close method is called to release all memory
allocated and all hardware set up by the component. If the Open method fails
for some reason and returns an error, the Component Manager calls the Close
method. This means the Close method must always check to see if there are
valid globals before using or disposing of them. The Close method also must not
access the hardware unless the InitOutputDevice method has been called, for
the same reasons described in "Pitfalls of the Open Method.
______________________________
The CanDo method is used to determine whether a selector is implemented by this
component. In our example code, the CanDo method calls the internal utility routine
GetComponentRoutine to determine whether a selector is implemented.
The Version method returns the version of the component, specified as a fixed-point
number. If you're using the auto-version feature of the 'thng' resource, this version
must agree with the one specified there.