Getting a 1980s computer to play samples

I have written about the Sharp MZ-700 before: it was the first computer I ever used properly, I taught myself BASIC on it from the excellent manual and had my first experiments in Z80 machine code as well.

After a recently conversation on the Twitters about polyphonic music from the ZX Spectrum, I wondered what the humble MZ-700 would be capable of. Was it possible to abuse the same techniques as the Spectrum to produce more complex music or even play sampled speech.

The 48K Spectrum made 1-channel sound with a simple piezo speaker driven directly by the CPU (later 128K versions would have a separate sound chip) driven through IO ports, so using the OUT z80 instruction. So it would be possible to play custom sounds by taking control of the CPU to set the speaker on or off.

This is where the Sharp differs. The Sharp has a speaker driven by an Intel 8253 programmable interval timer. This has three timers: one to count seconds, one to count 12 hours and one which generates square waves which are passed to the speaker to create sounds.

That counter, counter 0, is tied to an 895 kHz crystal, so you can play notes by passing the correct 16-bit value. By default the Sharp has a monitor routine to play a music string, which is composed of a text representation of the notes with tempo settings.

It also provides direct memory access to the 8253 registers at address $e004$e007. With $e004$e006 being read and write of the counters and $e007 being the control word (referred to as CONTF in the monitor source). This means we can program the 8253 to do what we want!

The default value for the 8253 control word is set by the routine MLDSP in the monitor, with the instructions:

LD A,36H
LD (CONTF),a ; E007H

The control word sets various values, by the bit patterns. $36 breaks out into the following bits:

$36 = 0011 0110
00 = Counter 0
11 = Read / Load LSB and the MSB
011 = Mode 3 (square wave generator)
0 = BCD off

This means we can send write the LSB of a 16 bit value, then the MSB of the 16 bit value to $e004 and it will generate a square wave of that frequency, for example a piano’s middle C (i.e. C4) is 261.63 Hz. As we have a 895 kHz clock, we can write 895 000 / 262 = 3416 to $e004 to play that note. Then send 1 to $e008 to enable the speaker (and 0 to disable it):

LD de,3416 ; value of middle C
LD a,e
LD ($e004),a ; LSB of note
LD a,d
LD ($e004),a ; MSB of note
LD ($e008),1 ; Turn sound on
CALL delay ; routine to wait for a bit
LD ($e008),0 ; Turn sound off

After some messing around trying to produce some polyphonic music (which sort of failed as I have no real musical ability, I was just trying to guess what stuff should sound like). I wondered whether it would be possible to play a simple sample. It was then I found this interesting article about playing samples on the PC speaker. The IBM PC also used an 8253 to drive the speaker, so was very similar to the Sharp.

Near the end there’s code to play standard PCM samples on a basic PC speaker. This uses mode 0 of the 8253, mode 0 basically sets the output to low for counts and then moves it to high. This gives total control of the PCM wave form.

So, hey, let’s try it. After some back-of-a-tab-packet calculations based on that 895 kHz clock I figured that a good sample frequency would be less than 8 kHz. So I recorded a very simple 8-bit unsigned 7 kHZ sample of me speaking in Audacity and exported it as a raw sample with not header (i.e. raw PCM data).

I altered my previous polyphonic code to just read bytes and push it to the 8253, and then tried it. And … it worked first time in the emuz-700 emulator! This was followed up when I asked the Twitter user @SharpworksMZ to try it on a real MZ-700 (mine is currently in bits). And … it worked first time, with an annoying whistle!

The code can be found in a gist on github, along with a very simple Python program to add a simple .msf header so it can work from emulated tape.

The code is surprisingly simple, a quick walk through:

          ld a,&30
          ld (pit8253cword),a
          ld a,&00
          ld (pit8253c0),a
          ld (pit8253c0),a
          ld a,&10
          ld (pit8253cword),a
          call musicon

This sets up the 8253, it initally sets channel 0 to take a 16-bit parameter (as LSB, MSB) and then sets the channel register to be 0x0000. It then resets channel 0 to only take an 8-bit value as the LSB. This is just to reduce the size of the sample – it’s still 16 kB even as an 8-bit sample!

          ld hl,sample
          ld bc,length

HL is set to the sample data; BC is the length of the sample. This leaves us DE as free registers which we’ll use in the delay function.

sampleloop: ld a,(hl)
          inc hl
          ld (pit8253c0),a
          call delay
          dec bc
          ld a, b
          or c
          jr nz, sampleloop

This is the bit that plays the sample – it loads the next octet from the sample data and sends it to the 8253. The it calls a very simple delay function to take up some CPU cycles whilst the 8253 plays the waveform and decreases the sample counter.

end:      call musicoff
          jp &00ad

End of code – turn sound off and return to the MZ-700’s monitor program. Really I ought to have reset the 8253 as well by sending 0x36 to the control word, but I’m lazy and the Sharp does this for you if you use the monitor’s sound routines.

; pause for value in de
delay:    ld de,(delaytime)
delayloop:dec de
          ld a, d
          or e
          jr nz, delayloop
          ret

The delay routine is just to read a value from delaytime and decrease it in a loop. This isn’t really that exciting.

; control sound generator a is not preserved
musicon:  ld a,1
          jr sendcont
musicoff: ld a,0
sendcont: ld (pit8253cont),a
          ret

The musicon and musicoff calls are just to push the right value to memory location which controls whether the output from the 8253 goes to the speaker.

pit8253c0:    EQU &e004
pit8253cword: EQU &e007
pit8253cont:  EQU &e008

length:       EQU 16010 
delaytime:    DEFW &10
sample:       MDAT "sample2.raw"

These are the memory addresses, length of the sample, delay time and sample data. I worked out the delay time, by altering memory in emuz-700 until it sounded right to me.

… And that’s it, playing a relatively decent sound sample on a computer built in 1983. As the Sharp uses the same programmable timer chip as in the IBM PC it should theoretically be able to play any of the polyphonic sounds that the PC can play. There’s a couple of caveats through: the RTC for the IBM PC was 2 MHz and the IBM PC’s CPU was clocked higher than the humble Sharp’s processor, so there may be a lot of timing of instructions needed.