See Part 2 for a discussion of decoding audio

See Part 3 to see real-time audio encoding/decoding used in conjunction with telnet.

My family's first computer was an Ohio Scientific Superboard II--something that my father purchased around 1979. At the time, the Superboard II was about the most inexpensive computer you could get. In fact, it didn't even include a power supply or a case. If you wanted those features, you had to add them yourself. Here's a picture of our system with the top of the (homemade) case removed so that you can see inside.

To say that the Superboard II is minimal is certainly an understatement by today's standards. There was only 8192 total bytes of memory and no real operating system to speak of. When you powered on the system you could either run the machine language monitor or Microsoft Basic Version 1.0. Here's a sample of what appeared on the screen (yes, that's maximum resolution):

Much to my amazement, our old Superboard II system stayed in the family. For about 20-25 years it sat in the basement of my mother's house surrounded by boxes. After that, it sat for a few years in a closet at my brother's condo. Occasionally, we had discussed the idea of powering it up to see if it still worked, but never got around to it--until now. About a week ago, my brother threw the old computer along with an old Amiga monitor in the back of his car and headed east to Chicago. After some discussion, we decided we'd just blow the dust out of it, power it on, and see what would happen.

Unbelievably, the machine immediately sprang to life. The above screenshot was taken just today. Since powering it up, I've written a few short programs to test the integrity of the memory and ROMs. Aside from a 1-bit memory error (bit 2 at location 0x861) it appears to be fully functional.

One problem with these old machines is that they had very little support for any kind of real I/O. Forget about USB, Firewire, or Ethernet. Heck, this machine didn't even have a serial or parallel port on it. In fact, the only external interface was a pair of audio ports for saving and loading programs on a cassette tape player--which was also the only way to save any of your work as there was no disk drive of any kind. Here is a picture of the back

Since the old machine seemed to be working, I got to thinking about ways to program it. Working directly on the machine was certainly possible, but if you look at the keyboard, you'll notice that there aren't even any arrow keys (there is no cursor control anyways) and some of the characters are in unusual locations. Plus, some of the keys are starting to show their age. For example, pressing '+' tends to produce about 3 or 4 '+' characters due to some kind of key debouncing problem. So, like most Python programmers, I started to wonder if there was some way I could write a script that would let me program the machine in a more straightforward manner from my Mac.

Since the only input port available on the machine was a cassette audio port, the proposition seemed simple enough: could I write a Python script to convert a normal text file into a WAV audio file that when played, would upload the contents of the text file into the Superboard II? Obviously, the answer is yes, but let's look at the details.

Viewing Cassette Audio Output



On many old machines, cassette output is encoded using something called the Kansas City Standard. It's a pretty simple encoding. A 0 is encoded as 4 cycles of a 1200 Hz sine wave and a 1 is encoded as 8 cycles of a 2400 Hz sine wave. If no data is being transmitted, there is a constant 2400 Hz wave. Each byte of data is transmitted by first sending a 0 start bit followed by 8 bits of data (LSB first) followed by two stop bits (1s). Click here to hear a WAV file sample of actual data being saved by my Superboard II. I recorded this sample using Audacity on my Mac.

Python has a built-in module for reading WAV files. Combined with Matplotlib you can easily view the waveform. For example:

>>> import wave >>> f = wave.open("osi_sample.wav") >>> f.getnchannels() 2 >>> f.getsampwidth() 2 >>> f.getnframes() 1213851 >>> rawdata = bytearray(f.readframes(1000000)) >>> del rawdata[2::4] # Delete the right stereo channel >>> del rawdata[2::3] >>> wavedata = [a + (b << 8) for a,b in zip(rawdata[::2],rawdata[1::2])] >>> import pylab >>> pylab.plot(wavedata) >>>

After some panning and zooming, you'll see a plot like this. You can observe the different frequencies used for representing 0s and 1s. Again, this plot was created from an actual sound recording of data saved by the system.

Converting Text into a KCS WAV File



Using Python's wave module, it is relatively straightforward to go in the other direction--that is, take a text file and encode it into a WAV file suitable for playback. Here is the general strategy for how to do it:



Define a utility function for making a square-wave pulse.

Define wave fragments for a 0-bit (4 cycles of a 1200 Hz square wave) and 1-bit (8 cycles of a 2400 Hz square wave).

Write a function that encodes a byte of data as an 11-bit wave fragment consisting of a start bit, 8 bits of data, and 2 stop bits.

Write a function that takes an input text file, encodes every single byte using this scheme, and writes a big WAV file with some extra padding on the front and back.

Here is a script kcs_encode.py that has one implementation.

##!/usr/bin/env python3 # kcs_encode.py # # Author : David Beazley (http://www.dabeaz.com) # Copyright (C) 2010 # # Requires Python 3.1.2 or newer """ Takes the contents of a text file and encodes it into a Kansas City Standard WAV file, that when played will upload data via the cassette tape input on various vintage home computers. See http://en.wikipedia.org/wiki/Kansas_City_standard """ import wave # A few global parameters related to the encoding FRAMERATE = 9600 # Hz ONES_FREQ = 2400 # Hz (per KCS) ZERO_FREQ = 1200 # Hz (per KCS) AMPLITUDE = 128 # Amplitude of generated square waves CENTER = 128 # Center point of generated waves # Create a single square wave cycle of a given frequency def make_square_wave(freq,framerate): n = int(framerate/freq/2) return bytearray([CENTER-AMPLITUDE//2])*n + \ bytearray([CENTER+AMPLITUDE//2])*n # Create the wave patterns that encode 1s and 0s one_pulse = make_square_wave(ONES_FREQ,FRAMERATE)*8 zero_pulse = make_square_wave(ZERO_FREQ,FRAMERATE)*4 # Pause to insert after carriage returns (10 NULL bytes) null_pulse = ((zero_pulse * 9) + (one_pulse * 2))*10 # Take a single byte value and turn it into a bytearray representing # the associated waveform along with the required start and stop bits. def kcs_encode_byte(byteval): bitmasks = [0x1,0x2,0x4,0x8,0x10,0x20,0x40,0x80] # The start bit (0) encoded = bytearray(zero_pulse) # 8 data bits for mask in bitmasks: encoded.extend(one_pulse if (byteval & mask) else zero_pulse) # Two stop bits (1) encoded.extend(one_pulse) encoded.extend(one_pulse) return encoded # Write a WAV file with encoded data. leader and trailer specify the # number of seconds of carrier signal to encode before and after the data def kcs_write_wav(filename,data,leader,trailer): w = wave.open(filename,"wb") w.setnchannels(1) w.setsampwidth(1) w.setframerate(FRAMERATE) # Write the leader w.writeframes(one_pulse*(int(FRAMERATE/len(one_pulse))*leader)) # Encode the actual data for byteval in data: w.writeframes(kcs_encode_byte(byteval)) if byteval == 0x0d: # If CR, emit a short pause (10 NULL bytes) w.writeframes(null_pulse) # Write the trailer w.writeframes(one_pulse*(int(FRAMERATE/len(one_pulse))*trailer)) w.close() if __name__ == '__main__': import sys if len(sys.argv) != 3: print("Usage : %s infile outfile" % sys.argv[0],file=sys.stderr) raise SystemExit(1) in_filename = sys.argv[1] out_filename = sys.argv[2] data = open(in_filename,"U").read() data = data.replace('

','\r

') # Fix line endings rawdata = bytearray(data.encode('latin-1')) kcs_write_wav(out_filename,rawdata,5,5)

You can study the implementation yourself for some of the finer details. However, most of the heavy work is carried out using operations on Python's bytearray object. For padding the audio, a constant 1 bit is emitted (a constant 2400 Hz wave). To handle old text encoding, newlines are replaced with a carriage return. Moreover, to account for the slow speed of the Superboard II, a pause consisting of about 80 bits is inserted after each carriage return.

To use this script, you now just need an old BASIC program to upload. Here's a really simple one (from the Superboard II manual):

10 PRINT "I WILL THINK OF A" 15 PRINT "NUMBER BETWEEN 1 AND 100" 20 PRINT "TRY TO GUESS WHAT IT IS" 25 N = 0 30 X = INT(RND(56)*99+1) 35 PRINT 40 PRINT "WHATS YOUR GUESS "; 50 INPUT G 52 N = N + 1 55 PRINT 60 IF G = X THEN GOTO 110 70 IF G > X THEN GOTO 90 80 PRINT "TOO SMALL, TRY AGAIN "; 85 GOTO 50 90 PRINT "TOO LARGE, TRY AGAIN "; 100 GOTO 50 110 PRINT "YOU GOT IT IN";N;" TRIES" 113 IF N > 6 THEN GOTO 120 117 PRINT "VERY GOOD" 120 PRINT 130 PRINT 140 GOTO 10 150 END

Let's say this program is in a file guess.bas . Here's how to encode it using our script.

bash $ python3 kcs_encode.py guess.bas guess.wav bash $ ls -l guess.wav 352652 bash $

Now, we have an audio file that's ready to go (note: it's rather impressive that a 476 byte input file has now expanded to a 350Kbyte audio file). You can listen to it here. Note that data doesn't start until about 5 seconds have passed.

Now, the ultimate test. Does this audio file even work? To test it, we first hook up the audio input of the Superboard II to my Macbook.

Next, we go over to the Superboard II and type 'LOAD'

Next, we start playing the WAV file on the Mac. After a few seconds, you see data streaming in (at about 300 baud). Excellent!

Finally, the ultimate test. Let's play the game:

Awesome! Note for anyone under the age of 40: yes, this is the kind of stuff people did on these old machines--and we thought it was every bit as awesome as your shiny iPad. Maybe even more awesome. I digress.

(It occurs to me that fooling around on this machine might be the reason why I got an F in 7th grade math and had to attend summer school)

Just so you can get the full effect, here is a video of the upload in action. It's really hard to believe that systems were so slow back then. For big programs, it might take 5 minutes or more to load (even with the 8K limit):







Well, that's about it for now. The power of Python never ceases to amaze me--once again a problem that seems like it might be hard is solved with a short script using nothing more than a single built-in library module and some basic data manipulation. Next on the agenda: A Python script to decode WAV files back into text files.

By the way, if you take one of my classes, you can play with the Superboard II yourself (wink ;-).