ZX Spectrum System Tour: Sound

ibobev1 pts0 comments

ZX Spectrum System Tour: Sound | Bumbershoot Software

Now that the shooting gallery is done and dusted, it’s time to turn to a piece of our initial tour of the ZX Spectrum that has been left untouched: sound. There isn’t a whole lot new here compared to what has come before on other platforms, but that in turn means that the core principles of our solutions to it arrive pre-baked; we’ll just have to add the Spectrum-specific frosting.

The mainline Spectrum systems offer two sound options:

A 1-bit beeper under software control. This is present on all systems, and the underlying hardware is not unlike what we saw on the Apple II or the PC speaker.

A General Instrument AY-3-8910 synthesizer chip, present in all models from the 128K Spectrum on and available as a peripheral for the 16K and 48K variants. These are basically the same as the one in the Atari ST but it’s clocked differently and we don’t have a built-in BIOS call to communicate with it.

This is part of our general survey, rather like our text mode and bitmap mode surveys, so our goals will be modest; we’ll end this article with three programs that play us a C-major scale.

Using the Beeper Through the BIOS

The 1-bit beeper is very similar to the Apple II’s; the $10 bit on the I/O port may be set or cleared to set the speaker’s status. Specific tones are produced by carefully timing the intervals between toggling that bit. We’ll get to code that does that later, but we can put that off because we don’t have to do it ourselves— the BEEP command in BASIC already manages this and it does so by forwarding to a generic routine that we may, ourselves, make use of. The BEEPER routine lies at memory location $03B5 and it’s honestly more clever than the versions I’ve written for 6502 systems. The HL register specifies the delay time in units of 4 cycles (the length of a no-op) and it acquires that precision using a technique not unlike the one we used for programmable cycle-exact 6502 delays. The DE register specifies the duration of the note, which is a touch inconvenient because it actually specifies the number of wavelengths the note should have. Notes of equal length but different pitches will need different duration codes.

The BEEP command itself, though, relies on being told notes in half-steps off middle C and durations measured in seconds. We can look at its own implementation, at $03F8 , to see how to get the values we need. This is done via the system’s floating-point engine, so we actually get neat formulae for both right out of the commentary:

The frequency code HL for a frequency of f Hz is provided by the formula 437500/f – 30.125.

The duration code DE for a note with frequency f Hz and duration t seconds is simply f×t.

A few lines of Python let us apply these to the frequencies of the C major scale, giving us frequency and duration codes to feed to the routine. Here is our playback program:

org $7000

ld b,8 ; Play 8 notes<br>ld hl,scale ; Load from pointer<br>loop: push bc<br>ld c,(hl) ; Load frequency into BC<br>inc hl<br>ld b,(hl)<br>inc hl<br>ld e,(hl) ; Load duration into HL<br>inc hl<br>ld d,(hl)<br>inc hl<br>push hl ; Stash pointer...<br>ld h,b ; ... Copy BC to HL...<br>ld l,c<br>call $03b5 ; ... and let the ROM do the rest<br>pop hl<br>pop bc<br>djnz loop<br>ret

scale: dw $066a,$0082,$05b3,$0092,$0511,$00a4,$04c6,$00ae<br>dw $043d,$00c3,$03c4,$00dc,$0357,$00f6,$0325,$0105

This is a "half-wavelength" system like my initial Apple II sound playback routine, but its much finer timing controls mean that the overall precision of the tones is much better. Essentially none of the tradeoffs I faced in my initial article show up here at all, and we have the kind of control we’d expect from a dedicated PSG. That’s honestly really great. The only real downside here is that it’s a little awkward to need to run our own divisions on both sides like this; if we want a generic routine that manages tones of arbitrary frequencies and duration we’ll need to bring our own divider. However, as we’ll see shortly, on a 16KB Spectrum we don’t have any other choice, and we have no option other than leaning on this routine.

We’re getting ahead of ourselves, though. Let’s look at how we’d do it by hand before actually running into the issues that we’d hit on the 16K systems.

Using the Beeper Directly

The BIOS routine is good enough that if if I’m going to write my own oscillator function I want it to run on an entirely different principle. Fortunately, I have just such a principle available: the 16-bit system based on frequency instead of half-wavelength that I created inspired by the internals of the SID and the Ensoniq chip. With the Z80’s built-in support for 16-bit math it should be much more straightforward than the 6502 code.

The basic idea is to repeatedly add our frequency code to a running counter and toggle the speaker every time that counter overflows. The tricky part is actually making sure that we don’t mess with anything else, because I/O port $FE does triple duty:

The low three bits...

spectrum routine duration frequency system sound

Related Articles