Thursday 24 January 2019

Focusing on Tape Integration

Foreword

In the previous post we managed to interface our C64 FPGA module with a USB keyboard.

In this post we will start to focus on tape integration to our C64 module. Well, not exactly interfacing with a 1530 Datassette, but simulating the tape loading process from a .TAP file.

While pondering in this alley, we might just relive the nostalgia a couple of decades ago where we all played a C64 cassette on a normal sound system to hear what it sounds like. For this exercise we will see if we can take a .TAP file and see if we can reproduce similar sounds, with the help of Python on a PC.

Once we have successfully reproduced the sound of a C64 tape, we will set forth and see if we can do the same on the Zybo board, with the logic implemented within the FPGA.

I will not be covering all the above mentioned in this post, but rather in several ones, working incrementally towards a solution where we have a fully integrated tape to C64 module solution.

The .TAP file format

Let us start by looking at the .TAP file format. For this exercise let us have a look at a snippet of a .TAP file:


The file header starts with a textual description C64-TAPE-RAW. The actual file data starts at offset 0x14.

The file data basically a set of pulse widths. In general a pulse width is represented by one byte. Multiply this value by 8, and you have the pulse width in terms of 1MHz pulses.

Let us have a look at our example snippet. Starting at offset 0x14, we see a series of 30's. Converting this number to decimal and multiplying by 8, we get 384. This gives us a period of 0.000384s.

From this period we can calculate the frequency from the equation f = 1/T. This gives us a frequency of 2604Hz. This is the monotone you hear for the first 10 seconds or so from a C64 tape.

Converting a TAP file to sound

With the information from the previous section, let us see if we can take a .TAP file and and generate the sound as we remember it a couple of decades ago.

For this exercise we will be using Python to generate the raw samples. Not many programs can play raw samples, but Audacity can play it.

Within Python we start off by opening the TAP file and moving to the byte position where the actual data starts:

import struct
f = open('Dan Dare.tap', 'rb')
resfile = open('file.dat', 'wb')
timei = 0
f.seek(20)

timei is the current time in millionths of a second. I will show in a moment how this variable gets updated.

The whole sound sample generation is driven by the following loop:

...
while timei < 240000000:
...

This loop will generate 4 minutes worth of sound samples.

Within the loop we start off by reading a pulse width:

...
while timei < 240000000:
  timeval = ord(struct.unpack('c', f.read(1))[0])
...

One thing I didn't mention earlier on is that a pulse byte value of zero is a special exception. A pulse byte value of zero means that an absolute time period value is to follow in the next three bytes. With this information in mind, we add the following code to our loop:

while timei < 240000000:
  timeval = ord(struct.unpack('c', f.read(1))[0])
  if timeval == 0:
    byte1 = ord(struct.unpack('c', f.read(1))[0])
    byte2 = ord(struct.unpack('c', f.read(1))[0])
    byte3 = ord(struct.unpack('c', f.read(1))[0])
    timeval = (byte3 << 16) + (byte2 << 8) + byte1
  else:
    timeval = timeval << 3


So, in this part we cater for both the zero byte time values and for other case.

We now have a physical time value, and hence we can update timei:

...
while timei < 240000000:
...
  timei = timei + timeval
...

We now have enough information for generating the sound samples. Keep in mind that each time period is broken down in two halves. In the first half our pulse have a positive value and in the second half our pulse have a negative value. For this reason it makes sense to work with half of the time period value, obtained by shifting the time value right by one bit:

...
while timei < 240000000:
...
  timeval = timeval >> 1


We would like to create sound samples at a rate of 48KHz, giving us the following code:

...
  timeval48khzfloat = float(timeval) * 48000/1000000
  timeval48khzint = int(timeval48khzfloat)
  for x in range (timeval48khzint):
    resfile.write(struct.pack('h',32000))
  for x in range (timeval48khzint):
    resfile.write(struct.pack('h',-32000))
...

This code will generate the sound samples for us. Finally, we just need to close the file when we are done:

resfile.close()

Listening to the result

I took the samples and converted it to a mp3 with the help of Audacity.

Unfortunately, since I use Blogger for hosting my posts, there is not a easy way to embed sound clips within posts. So I had to create a video from the mp3 and upload it to Youtube so everyone can listen to the end result.

It is perhaps advisable to tune down on the volume when listening to this, since there is some tones that can be annoying to the ear:

It sounds more or less as I remember it when I listened a couple of decades ago on a tape deck to C64 tape. Perhaps the leading mono-tone sounds too pure compared to the tape player of the day.

In Summary

In this post we have started to investigate how to integrate tape loading functionality to our C64 module.

As a nostalgic exercise, we attempted to reproduce the sound of a .TAP file as we remember it long time ago.

I performed this exercise on a PC with Python and Audacity.

It would be interesting to see if this exercise can be performed on a Zybo board, taking the .TAP file and generating the sound samples in real time within the FPGA and outputting the sound to a speaker, via the Line Out on the Zybo board.

My goal of generating sound from the .TAP file on a Zybo board perhaps sounds a bit over the top and unnecessary, but it can be an opportunity to learn how to use sound on the Zybo board. This knowledge be valuable if we later decide to also incorporate a SID within our C64 module.

So, in the next post we will attempt to generate sound on the Zybo board.

Till next time!