Archive

Posts Tagged ‘sound’

How To Write ZX Spectrum Games – Chapter 16

October 2, 2013 Leave a comment

Music and AY Effects

Note: This article was originally written by Jonathan Cauldwell and is reproduced here with permission.

The AY-3-8912

Introduced with the 128K models and pretty much standard since then, this is a very popular sound chip, used on various other computers, not to mention video games and pinball machines.  Basically, it has 14 registers, which can be written to, or read from, via in and out instructions.

The first six registers control the tone for each of the three channels, and are paired in the little-endian way we would expect, ie register 0 is Channel A tone low, register 1 is channel A tone high, register 2 is channel B tone low, and so on.  Register 6 controls the white noise period, values of 0 to 31 are valid.  0 gives the highest frequency noise, 31 the lowest.  Register 7 is the mixer control.  Bits d0-d5 select white noise, tone, neither, or both.  To enable tone or white noise, a bit must be reset, so 0 outputs tone and noise from all three channels, 63 outputs nothing.  Registers 8, 9 and 10 are envelope/amplitude controls.  A value of 16 tells the chip to use the envelope generator, 0-15 will set the volume for that channel directly.

In practice, you are better off controlling the volume yourself, along with the tone.  By varying these from one frame to the next, it is possible to produce a variety of very good sound effects.  Should you wish to use the envelope generator, the next two registers, 11 and 12, are paired to form the 16-bit period of the envelope, and register 13 determines the pattern.  The 128K manual explains the full list of patterns, but I won’t cover them here as I have never found them particularly useful myself.

To read a register, we write the number of the register to port 65533, then immediately read that port.  To write to a register, we again send the number if the register to port 65533, and then the value to 49149.  To the uninitiated, Z80 opcodes don’t appear to be capable of writing to 16-bit port addresses.  Don’t let that confuse you, it’s just that the way they are written is misleading.  out (c),a   actually means   out (bc),a   and   out (n),a   actually does   out (a*256+n),a.

Reading a sound chip register does have some uses.  You might want to read the volume registers 8, 9 and 10 and display volume bars – I did something along those lines in Egghead 5.  Also, believe it or not, the Sinclair light gun is read via sound chip register 14.  Yes, really.  It only actually yields two pieces of information, whether or not the trigger is pressed, and whether or not the gun is pointed at a bright part of the screen, or indeed any bright object.  It is up to the programmer what he does with that information.

Here’s some rather basic code to write to the sound chip:

; Write the contents of our AY buffer to the AY registers.

w8912  ld hl,snddat        ; start of AY-3-8912 register data.
       ld e,0              ; start with register 0.
       ld d,14             ; 14 to write.
       ld c,253            ; low byte of port to write.
w8912a ld b,255            ; 255*256+253 = port 65533 = select soundchip register.
       out (c),e           ; tell chip which register we're writing.
       ld a,(hl)           ; value to write.
       ld b,191            ; 191*256+253 = port 49149 = write value to register.
       out (c),a           ; this is what we're putting there.
       inc e               ; next sound chip register.
       inc hl              ; next byte to write.
       dec d               ; decrement loop counter.
       jp nz,w8912a        ; repeat until done.
       ret

snddat defw 0              ; tone registers, channel A.
       defw 0              ; channel B tone registers.
       defw 0              ; as above, channel C.
sndwnp defb 0              ; white noise period.
sndmix defb 60             ; tone/noise mixer control.
sndv1  defb 0              ; channel A amplitude/envelope generator.
sndv2  defb 0              ; channel B amplitude/envelope.
sndv3  defb 0              ; channel C amplitude/envelope.
sndenv defw 600            ; duration of each note.
       defb 0

By calling w8912 once every iteration of the main loop, the sound is constantly updated. It is then up to you to update the buffer as each noise changes from one frame to the next. Think of it as “animating” sound. However, just because you stop updating the sound registers the sound won’t stop playing. The AY chip will keep playing your tone or noise until instructed to stop. A quick way to do this is to set the three amplitude registers to 0. In the example above, write a zero to sndv1, sndv2 and sndv3 then call w8912.

 

Using Music Drivers

Most 128K music drivers, and some 48K ones, have two entry points.  An initialisation/termination routine which stops all sound and resets the driver to the beginning of the tune, and a service routine to be called repeatedly, usually 50 times per second.  A good place to store music is usually 49152, the start of the switchable RAM bank area.  If you know the start address of the driver, or are in a position to determine this yourself, the very beginning is usually the initialisation address which sets up your music at the start.  More often than not, the code at this point either simply jumps to another address, or loads a register or register pair before jumping elsewhere.  The service routine tends to immediately follow this jp instruction.  If your driver has no other documentation, you may have to disassemble  the code to find this address, usually 3-6 bytes in.

To use a music driver, call the initialisation address prior to starting, and also when you want to turn it off.  Between these points, you need to call the service routine repeatedly.  This can either be done manually, or by setting up an interrupt to do the job automatically.  If you choose to do this manually, for example in menu code, bear in mind that clearing the screen and displaying a menu, high score table, instructions etc will take more than 1/50th of a second to do, so this will delay your routine and could sound odd.  It might be better to write a routine to clear the screen over several frames with some sort of special effect, punctuated with halts and calls to the service routine every frame.