Sound Made Simple
Volume Number: 2
Issue Number: 2
Column Tag: Pascal Procedures
Mac Sound Made Simple 
By Alan Wootton, President, Top-Notch Productions, MacTutor
Contributing Editor
This month we will attempt to coax some sounds from the Mac. We will try all
three of the sound producing modes on the Mac. And, since there is a problem, I will
present what I think is a better Pascal interface to the Sound Driver. Finally, I will
introduce a method of saving sound effects in a standard form for later use.
The Sound Driver
The first source of information should be the Sound Driver chapter of Inside
Macintosh. The first apparent fact about the Sound Driver is that it supports three
different forms of synthesis. All three modes are reached by calling PROCEDURE
StartSound( synthRec : Ptr ; NumBytes : LONGINT ; completionRtn : ProcPtr ) with
synthRec set to the proper values. Since StartSound is defined in all three Pascals we
are likely to use (Lisa Pascal, TML Pascal, and MacPascal; On-Stage Pascal was not
available at the time of this writing), we will work in MacPascal and assume that our
work is easily transportable to the others. As a driver, the Sound Driver will accept
standard Device Manager control calls. There are predefined routines that make the
driver calls for us in most cases. The Sound Driver is in the ROM and never needs an
Open call (or a Close). As drivers go, it is fairly simple to deal with, so we won't go
into extreme detail. Advanced users will want to use completion routines and other
driver features.
Before we start making our first sound, it will be worthwhile to acknowledge
how it is that the Mac can make sound at all. There is a special area in RAM where
sound information is stored (much as there is a designated area of RAM that holds the
screen information). Every time the video circuitry has completed painting one line on
the screen, a byte is fetched from the sound RAM, and sent to a digital-to-analog
converter. From there, the analog signal is sent to the speaker. A sound byte is
processed 370 times for each 1/60th of a second. There are 60 'ticks', and also 60
frames of Mac video every second. This means that every second, 60*370=22,200
bytes, are processed by the sound circuitry. The Sound Bytes are meant to represent an
output voltage at the speaker, with 255 being greatest and 0 being smallest (we will
use 128 for a neutral point). So you see, the Mac is not much of a synthesizer at all.
All it really can do is play back data. Any other functions desired will have to be
provided for in software. Every tick the same 370 bytes in memory are used, so unless
we want to hear the same thing over and over, 60 times per second, we will have to
replace all 370 bytes repeatedly. If we had to do this ourselves we'd be talking
assembly language now. Fortunately, the Sound Driver is in assembly language and it
will do this for us. And it will do it three different ways.
Our main goal is to simply get program shells of the three sound modes in action,
and to defer until later those data structures and routines one may use in a complex
application.
The Square Wave mode
The square wave mode of synthesis appears to be the simplest. All the Sound
Driver has to do, 60 times per second, is fill the sound buffer (370 bytes) with
alternating values. If every other byte were 0 and 255, then we would get the loudest
sound and the highest note (370/2 alternations per tick* 60 ticks per sec = 11,100
Hz). If one tick the 370 bytes were all 128, and the next tick they were all 129, we
would get a very quiet note at 30 Hz. (You probably couldn't hear it from the Mac
speaker). I don't think this is actually how the Sound Driver does it, but you get the
idea.
Here is a MacPascal program that should get some different tones using the
Square Wave Mode.
program Square_Wave_Scale;
const
notecount = 31;
var
i : integer;
MySWSynthRec : record
mode : integer;
tones : array[1..notecount] of record
count, amplitude, duration : integer;
end;{ of tones }
end;{ of mySWSynthRec }
begin
with MySWSynthRec do
begin
mode := SWMode;
for i := 1 to notecount do
with tones[i] do
begin
count := i * 64 + 256;
amplitude := 255 - i * 8;
duration := 30;{ 1/2 sec. }
end;
StartSound(@MySWSynthRec, sizeof(MySWSynthRec), nil);
ShowText;
Writeln('press mouse to stop');
repeat
until button;
end;
StopSound;
end.
The problem is that Square_Wave_scale does not work. The SWSynthRec is
filled out correctly; the mode is set, and the tones are filled in. All is well, but there
seems to be something wrong with StartSound. The likely culprit is the CompletionPtr
parameter. According to the MacPascal manual, you pass the address of a procedure if
you want that procedure to be executed when the sound is done. MacPascal will
( currently) not allow you to get the address of any of its procedures. If you want the
sound to start, and then finish before the program resumes, you are to use
pointer(-1). When I did that the Mac locked up, and I had to reboot. If you want the
sound to start, and then for the program to resume before the sound is done (seems like
a good idea), use nil. When I did that nothing happened. Perhaps we should junk
StartSound.
This brings up an interesting point. Clearly the point of this completion routine
business is to be able to splice two sounds front to back with no seam. If our program
has to wait for the first sound to finish, then there will be a delay (e specially in
MacPascal) before the next sound can be started. StartSound is going to have to make a
Device Manager call (in this case PBWrite) to start the sound. To do that it will need to
use a Parameter Block Record. What happens to that record when a second sound is
started if the first is not done?
What must be done is to create our own version of StartSound. We will use two
parameter block records. That way one can be filled out and then passed to the device
managers I/O queue while the other is busy. Our sounds will follow one another
naturally without much fuss or muss. The predefined procedure 'Generic' will be used
to make a Device Manager call. Let's cover this strategy in more detail.
First, declare the procedure. We will call it myStartSound, and use the same
parameter list, even though completionRtn will not be used.
Second, the Parameter Block Record. Instead of using the entire record
declaration, found in the Device Manager chapter of Inside Macintosh, we will use an
array of integers and use BlockMove to move data into the record. Note that the
numbers used alongside the Device Manager routine descriptions must be divided by two
to get an index into the array of words. Only ioRefNum, ioBuffer, and ioReqCount are
used, so this is reasonable. You must declare the parameter blocks at the outermost
level possible, ie. they must not go away because the procedure they are defined in is