Sound Wired
Volume Number: 1
Issue Number: 9
Column Tag: Assembly Language Lab
"Wired for Sound"
By Chris Yerga, MacTutor Contributing Editor
Can You Keep a Secret?
One of the Macintosh's best kept secrets is its wonderful sound driver. This
beauty is capable of generating a variety of sounds including simple tones,
multi-voice music, complex speech, and digitally recorded sounds. In spite of its
power, it is relatively easy to use once the programmer takes the time to understand a
few basic concepts. This month's column concerns itself with the sound driver and the
techniques the programmer must employ to use it. First will come a description of the
device manager, with emphasis on it's relation to the sound driver in particular. The
actual sound driver description is next, followed by a sample program that ties it all
together.
First Things First
First, a bit of background information on the device manager. The device
manager is the part of the Macintosh ROM that allows the use and control of devices,
which are usually hardware peripherals connected to the Mac. Examples of devices are
the serial ports, the disk drives, the sound driver, etc (desk accessories are also
considered devices, however, we will not include them in our general discussion).
There are two types of devices: character and block. Character devices can read
or write only one character at a time and they must do so sequentially. That is, they
cannot access any data other than the very next character. Whereas block devices read
and write data in 512 character blocks and are randomly accessible. This means that
they have access to any block of data, regardless of its position. Another matter of note
is that many device manager routines may be executed either synchronously or
asynchronously. Synchronous execution means that the calling program remains idle
while the requested I/O operation is performed. Conversely, asynchronous I/O
operations are performed while the calling program is running.
Now Some Specifics
The device manager is one of the portions of the Macintosh ROM that has a
register based calling interface. As you may remember from previous columns, this
means that the programmer invokes the desired trap macro with address register A0
pointing to a data structure in memory. It is through this data structure that
parameters are passed between the calling program and the routine.
The data structure that the device manager routines use is called the
ioParamBlock (from here on abbreviated as ioPB). Figure 1 shows the structure of
the ioPB. The numbers in parenthesis are the byte offsets from the base address of the
ioPB. There is a byte-division ruler to the right of the figure to ease the judgement of
lengths.
The programmer need not concern himself with the first four fields of the ioPB,
the device manager routines use them internally.
The fifth field, ioCompletion, is used for asyncronous I/O operations. When the
device manager has finished an asyncronous operation, it will jump to the routine
pointed to by ioCompletion, if it is nonzero. For example, you could request that the
device manager send a page of data asynchronously through the serial port. While the
device manager was doing this, you could set up the next page of data to be sent. When
finished, the device manager would jump to your completion routine, which in turn
would send the next page of data.
Upon returning from a device manager call, the ioResult field will contain a code
describing what error, if any, occurred. IoNamePtr is only used when first opening a
device. It points to the name of the device to be opened. All open devices are assigned
reference numbers by the device manager. These reference numbers must be specified
by the calling program and are stored in the ioRefNum field.
The device driver allows Control/Status calls to certain devices. These calls
allow the user to send commands to devices (such as configuration commands) or to get
status information from devices. A control/status call requires that the programmer
send a control code to the device; this code is contained in the CSCode field of the ioPB.
Certain control calls also need parameters of their own. These parameters are passed
through the CSParam field.
When a read or write call is made, the address of the data buffer is passed in
ioBuffer. The number of bytes that the programmer wants to read or write is
contained in the ioReqCount; the number of bytes successfully read or written is
returned in ioActCount. IoPosMode and ioPosOffset are used only with block devices.
They allow the data to be accessed non-sequentially.
To use the sound driver from assembly language, the programmer must set up
the appropriate fields of the ioPB, load A0 with a pointer to it, and then call the trap
macro. Asyncronous execution is flagged by appending ",ASYNC" to the end of the trap
name. For example to write a data buffer to a device, you would set up it's reference
# etc. in the ioPB, store a pointer to the buffer in the ioBuffer field, load A0, and
then execute _Write,ASYNC.
The sound driver is broken into three pieces: the square-wave synthesizer, the
four-tone synthesizer, and the free-form synthesizer. Each synthesizer generates a
different type of sound and requires a specific amount of processor time. Each type of
sound is generated by making a write call to the device manager requesting that the
proper data is sent to the sound driver. The square-wave and four-tone synthesizers
will be described here.
Nothing Fancy
The simplest subset of the sound driver is the square-wave synthesizer. When
running asynchronously, the square-wave synthesizer uses approximately 2% of the
processor's time- a modest degree of overhead. However, the type of sounds that this
synthesizer is capable of creating are limited to simple tones or beeps.
To get the square-wave synthesizer going, the programmer simply writes a
SWSythRec to the sound driver. The first word of the SWSythRec is -1. This tells the
sound driver that the data in the record should be routed to the square-wave
synthesizer. Following this is a list of tones to be produced. The end of the tone list is
dentoed by a zero.
Each tone is 3 words long. The first word is the count value, where frequency =
783360 / count. Complete lists of count values for the C major scale and the equal
tempered scale are contained in Inside Macintosh. The second word is the amplitude,
or volume, of the tone. This value ranges from 0-255 inclusive, with 255 being
maximum volume. The last word is the duration of the tone, in 1/60ths of a second.
An example set of data will make the above explanation clearer. The sample will
be 3 tones with frequencies of 1000, 2000, and 4000 Hertz. The amplitude of the
tones will be half volume. The lengths of the tones will be .5, 1, and 1.5 seconds.
The count values are calculated by dividing 783360 by the frequency.
783360/1000 = 783; 783360/2000 = 392; and 783360/4000 = 196. The
amplitude will be 127 ( half of 255 ). Given that 1 second = 60 duration units,
calculating the duration values is done by: (.5 )* 60 = 30; 1 * 60 = 60; 1.5 * 60 =
90.
The data looks like this:
A Barbershop Quartet in Your Mac
The four-tone synthesizer is the most taxing in terms of processor use-
approximately 50%. However, it allows up to 4 tones to be produced simultaneously,
allowing multi-voice music and chords to be produced. Its record, FTSynthRec, has
the most complex structure of the three synthesizers.
The first word of data in the FTSynthRec is always a 1. Following this is the
duration of the sound, again in 1/60ths of a second. Next are 4 pairs of values for
each of the four tones to be produced.
The first long word of each pair is the rate value, a fixed-point number which is
analogous to the SWSynthRec's count value. (A discussion of fixed-point numbers can
be found on page 11 of the Memory Manager Introduction in Inside Macintosh ) In this
case, the frequency is derived by the formula: frequency (Hz) = 1000000 /
(11502.08 / rate). The last element of the pair is a long integer phase value. The
phase can range in value from 0-255, and tells the square-wave synthesizer how
many bytes in the tone's waveshape definition to skip before starting the tone (If this
confuses you, don't worry about it. Great sounds can be created without any regard for
phase).
After the four pairs of values come four Waveshape pointers, which point to
waveshape definitions in memory. Each waveshape definition consists of 255 bytes
that describe the shape of one pulse, or click, sent to the speaker. Consider a tone of
any frequency. It is comprised of a series of pulses, or "clicks", that create a tone
when generated at a certain rate. The pulse itself is a general unit used by all tones,
and therefore has no effect on the frequency of the resulting tone if it is designed
correctly. Figure 3 shows the waveform description of a square pulse. Although it is
the simplest possible waveform to generate, it is not as "natural" sounding as a sine
pulse. In most cases, a square pulse or a sine pulse will suffice; however, by
changing the wave definition, various musical instruments can be simulated.
A C major chord will suit well for a set of sample data. This chord is comprised
of the notes C, E, and G. The table in Inside Macintosh gives us fixed-point rate values
of 3.03654, 3.79568, and 4.55481 for these notes. The equivalent hex values for
these numbers are $030959, $03CBB0, and $048E06 respectively. A duration
value of 60 will generate the sound for 1 second. Using the default phase value of 0,
the only remaining task is the initialization of the waveshape definitions and pointers.
The sample waveshape definition is created by filling a 255 byte block of
memory with data - the first half (bytes 0-127) with the value 255 and the last half
(bytes 128-255) with the value 0. When this is done, the address of the block of
memory is stored in the waveshape pointers of our three tones.
Here is a sample FTSynthRec with the data for a C major chord:
There are two other differences that make the four-tone synthesizer different
from the square-wave synthesizer. After the FTSynthRec is set up in memory, it is
not directly written to the sound driver. Rather, a six byte block is written. The
first two bytes comprise an integer word of value 1. The last four bytes of this block
contain a pointer to the actual FTSynthRec in memory. The second difference is that,
by nature of this calling scheme, only one FTSynthRec can be written at a time.
Therefore, it is necessary to loop if there is more than one sound to generate.
The program gives examples of using both the square-wave synthesizer and the
four-tone synthesizer. One useful item is the macro named Center, which when given
a string, the center X coordinate of the grafport, and the Y coordinate at which the text
should appear, will draw the text centered at the given Y coordinate. This saves a
great deal of "hit-and-miss" experimentation if, like me, you don't sit down and
calculate the correct centers by hand.
With this information and a bit of patience, the average reader should be able to
implement sound in his next assembly application to give it that special touch of style.
;◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊
;◊◊ Sound Example #1 ◊◊
;◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊
;
; © 1985 By Chris Yerga for MacTutor
INCLUDE MacTraps.D
; Declare external labels
XDEF START
; Define Macros
; Note that this clever macro will center a
; string for you about the MidPT, on line Y
; for use with the _DrawString trap call.
MACRO Center String,MidPT,Y =
CLR.W -(SP) ;Save room for INTEGER
;width of string
PEA '{String}'
_StringWidth
CLR.L D3 ;Clear the high word of
;D3 so the DIVU works
MOVE.W (SP)+,D3 ;Get the width (in pixels) in D3
DIVU #2,D3 ;Divide by 2
MOVE.L #{MidPT},D4
SUB.W D3,D4 ;Subtract (Width/2) ;from center point
MOVE.W D4,-(SP) ;Push the X cordinat
MOVE.W #{Y},-(SP);Push the Y cordinat
_MoveTo ;Position the pen
PEA '{String}'
_DrawString
| ;End of Macro symbol
;*** Local Constants ***
AllEvents EQU $0000FFFF
MaxEvents EQU 12
DWindLen EQU $AA ; see DS storage area
; Start of Main Program
BadPtr: _Debugger ;Should never get ;here. Is
called when there is ;a problem with the memory
; manager.
START: MOVEM.L D0-D7/A0-A6,-(SP) LEA SAVEREGS,A0 MOVE.L
A6,(A0) MOVE.L A7,4(A0)
;Initialize the ROM routines
PEA -4(A5) ;QD Global ptr
_InitGraf ;Init QD global
_InitFonts ;Init font manager
_InitWindows ;Init Window Mgr
_InitMenus ;Guess what...yes!
CLR.L -(SP)
_InitDialogs
_TEInit ;Init ROM Text edit
MOVE.L #AllEvents,D0