[net.micro.apple] Complex Sound w/o extra hardware

sdh@joevax.UUCP (The Doctor) (08/26/86)

There have been a few postings recently from people asking questions about
how to get good sound from the Apple (with peripherals), so I'm posting
a few 6502 sources for producing sound from the Apple's speaker and getting
quality beyond what one would think possible.

The traditional way of doing sound routines on the apple is to take the
point of view, All I have to do is write a loop that clicks the speaker,
and does a delay.  The number of times through the loop is the duration,
and the delay will generate the frequency.
Such a routine looks like this:

SPKR	.EQ $C030
FREQ	.EQ $00
DUR	.EQ $01
SOUND	BIT $C030 ; CLICK SPEAKER, BIT DOESN'T ALTER REGISTERS
	LDA FREQ
	LSR  	  ; PUT FREQ/2 IN X 
	TAX
.1	LDY FREQ  ; PUT FREQ IN Y
.2	DEY	  ; DECREMENT Y UNTIL ITS ZERO
	BNE .2
	DEX	  ; DO THE SAME FOR X
	BNE .1
	DEC DUR
	BNE SOUND
	RTS

A very simple routine, and very useable, but it has a few fundamental problems,
the use of a delay as the determinant of frequency causes the duration to
decrease as the frequency increases, that is higher pitches will have much
shorter apparent durations, even though they will go through the main loop
the same number of times as lower pitches.

The trick here is to get a routine that isn't delay dependant.  One way to
think about this process is to remeber that frequency is defined as the number
of cycles per unit time. In our case, the unit of time is trips through
the main loop, and the cycle is the speaker being clicked. So instead of
clicking the speaker every time through, we'll click every nth time where
n becomes the reciprocal of the frequency, or the period.

Here is a simple routine to do that.

SPKR	.EQ $C030
FREQ	.EQ $00
DUR1 	.EQ $01
DUR2	.EQ $02  ; Using two bytes to get longer durations.
SOUND	LDY FREQ ; Put frequency in y 
SND1	BEQ EXIT ; Zero frequency makes no sense here
	DEY	 ; Decrement Y
	BNE NOCLCK ; No click if not zero
	BIT SPKR
        LDY FREQ   ; To get here, y must have reached zero, so we reload
NOCLCK  DEC DUR1 
	BNE SND1
	DEC DUR2
	BNE SND1
EXIT	RTS

Now we have a routine that not only insures that low frequencies are the
same durations a high frequencies, there is also a much better function for
determining frequency.  If you choose, it is not hard to come up with a
formula that comforms to that of a just scale, but there will be some
error in it towards the high end of the range, since the the branches will
take a variable amount of processor cyles depending on whether or not they
are successful.  This is a minor inconvenience compared with the problems
in the first routine.

The duration was set to two bytes this time because very little time is
spent in the main loop, so longer durations are imperative.
The fact that there is so much extra time tells us that we could probably
duplicate the actual frequency routine,	providing us with 2 simultaneous
voices. Such a program would look like this:

SPKR	.EQ $C030
FREQ1	.EQ $00
FREQ2	.EQ $01
DUR1	.EQ $02
DUR2	.EQ $03
SFR1	.EQ $04 ; scratch frequency
SFR2	.EQ $05 ; this will free up our registers.
SOUND	LDA FREQ1
	BEQ EXIT
	STA SFR1 ; Avoid zero frequencies
	LDA FREQ2
	BEQ EXIT ; Ditto
	STA SFR2
LOOP	DEC SFR1
	BNE NCLK1 ; Only click if zero
	BIT SPKR
	LDA FREQ1 ; restore old frequency
	STA SFR1
NCLK1	DEC SFR2
	BNE NCLK2 ; Same as above, but with second frequency;
	BIT SPKR
	LDA FREQ1 ; restore old frequency
	STA SFR2
NCLK2	DEC DUR1
	BNE LOOP
	DEC DUR2
	BNE LOOP
EXIT	RTS

This routine gives us 2 voices of equalized duration that now only changes
the accumulator, letting you use the x and y registers for other things
(like indexes to tables of frequencies and durations for music) without
having to waste time saving them.  For something really impressive, try
using $FE and $FF for the frequencies.  This gives you a very deep tone
that sounds similar to a sound used on Williams arcade games (Defender,
Stargate, Robotron etc).  If you want an additional parameter, eliminate the
first STA SFR1 and STA SFR2 instructions, this will make the initial value
of SFR1 and SFR2 the phase of the waveform generated.

There is another little quirk about the Apple's speaker we can take advantage
of. Tray the following program:

SPKR	.EQ $C030
SOUND	BIT SPKR
	JMP SOUND  ; Loop forever

This program appears to make no noise at all!  If you listen very closely (you
may have to remove the cover of the machine) you will hear a very faint
tone.  The reason its so quiet is because that of the actual mechanics of the
speaker.  It only makes a click every other time its soft switch is referenced.
Whene it clicks, the machine is pushing the speaker cone out (I think) and the
other time its pulling it back in.  If you get a second click very soone after
the first, you choke off the sound -sort of like hitting a cymbal and
dampening it immediately.  By controlling how soon after the speaker has been
clicked that we dampen it, we can control the volume IN SOFTWARE!

Here is a routine to do that.  For speed purposes, the code must be
self-modifying, otherwise its gets tough to control the frequency accurately.

SPKR	.EQ $C030
FREQ	.EQ $00
SFRQ	.EQ $04  ; scratch frequency
DUR1	.EQ $01
DUR2	.EQ $02
VOL	.EQ $03
SOUND	LDA VOL
	AND #$0F;  Limit the user to 16 volumes
	TAY	;  Put in Y so we can incrememnt (faster than adding 1)
	INY	;  guarantee non-zero volume
	STY VENTR+1 ; Store in volume entry location
	LDA #$11
	SEC
	SBC VENTR+1 ; Subtract volume level for equalization
	STA EQUAL+1 ; Put in equalizer
	LDA FREQ
	BEQ EXIT ; Don't do a zero frequency
        STA SFRQ ; Put in scratch
LOOP	DEC SFRQ
	BNE NCLCK ; Only click if zero
	BIT SPKR
VENTR	LDY #$00  ; doesn't matter what value we put here. It gets set above.
LOOP1	DEY
	BNE LOOP1 ; VERY short delay. Won't affect frequency if we equalize
	BIT SPKR  ; Muffler.
EQUAL   LDY #$00  ; This value is also set above.
LOOP2	DEY
	BNE LOOP2
	LDA FREQ  ; Reset frequency
	STA SFRQ
NCLCK	DEC DUR1
	BNE LOOP
	DEC DUR2
	BNE LOOP
EXIT	RTS

The reason for the equalizer is that we are doing a delay right after we click
the speaker. If we're not careful to make sure that the section from VENTR
to NCLCK takes the same amount of time, no matter what the volume, it will
have a strong effect of the frequency.  What we do is, if the volume is 1,
we do a delay of 1 before the second click, and a delay of $10 after for a
total of 11 (A 2 will have an equalization delay of $0F, 3 will have $0E
and so on).  This means that the same value for the frequency will have the
apparent frequency at all volumes (this isn't completely true since the
equalization is only a greedy approximation, but close enough).

That's all I'm going to write on this subject.

Other things to try are:

2 voices with independant volume controls
Dynamic volume control
Attack & Decay

These are a little tricky, but not too hard.

Steve Hawley
joevax!sdh

PS Does this mean I'm an Apple II guru?