CosmacCalc: The Cosmac VIP’s place in Spreadsheet History

0414151223a
What does a Cosmac VIP have to offer to an exhibit on the history of spreadsheets?  That’s a really good question with an interesting answer.

My exhibit for Vintage Computer Festival East 10.0 chronicled about 8 years of the early spread sheet “killer app” phenomena that began with VisiCalc in 1979.  It required the service 11 computers- almost everything that I had restored over the last two years.  Much thought and research went into what products to show on what computer.  Commercial releases were located for each of the interesting machines from ebay and disk image sources.  Quite a few late night hours were spent pouring over various spreadsheet history writings on the web.

I had read about Dan Bricklin first demoing his spreadsheet concept using Integer Basic on an Apple II.  Well wouldn’t that be cool to exhibit?  Yes, but I saw no evidence that the demo software still existed.  There was even question if the original VisiCalc source code existed.  The answer we learned later from Bob Frankston (who was one of our keynote speakers) is it may be in the bowls of IBM somewhere.  Further, I had no Apple ][ with integer basic to mock a convincing demo up with.

About two weeks before the show I noticed one of my prominent pieces not playing a role in the exhibit- the Cosmac VIP.  That’s because it didn’t play a role in interactive spreadsheet history- it was a hobby & educational single board computer! But what, I thought, if it had? [NOTE: Tom Pittman had a NON-interactive spreadsheet example in one of his books where you set up the sheet with data statements. This is more like the kinds of fixed spreadsheet solvers that predated VisiCalc on timeshare systems – a key innovation of VisiCalc was interactivity.]

But what if someone had demonstrated interactive spreadsheet functionality on the VIP instead of the Apple ][- could microcomputer history have been changed?   So what kind of interactive spreadsheet demo might be possible with this meager of a system:

0414151240

So I decided to show SOMETHING on the Cosmac VIP as part of the exhibit that evoked this question.  It was not lost on me, that after all, the chief role of spreadsheets were to let us ask “What If…”.

What would I write it in?  My first thought was a good 1802 assembler, but then I thought, why not go right to C, using Bill Rowe’s C compiler work with Olduino?  I might have gone this route except I perceived (not necessarily correctly) that I would have to create the video support for the VIP’s 1861.  Being short on time I continued to look for options.

So poking around the web for ideas (saying “surfing” feels so quaint now)  I came across Octo – a block structured language that compiles down to the op codes of a very old game language called CHIP-8.  CHIP-8  programs are executed by a CHIP-8 interpreter- or in todays lingo a “virtual machine”.   CHIP-8 was authored for the Cosmac VIP by Joeseph Weisbecker, who designed the RCA 1802 processor.  The machine code listing for the interpreter was provided in the Cosmac VIP manual.  Since then a number of different interpreters have been made for many computing platforms including, of course, smartphones.  CHIP-8 may in fact be the original “game engine”.  Writing a CHIP-8 interpreter is a fine computer science exercise.

And Octo is a wonderful abstraction of CHIP-8 .  It is written in JavaScript and the author provides web based IDE that with the basic tools you need to develop a good CHIP-8 program – including a debugger, a sprite editor, and a built in CHIP-8 emulator.  VERY NICE.

Octo

The question was, can CHIP-8 really do what I envision my demo to be? If so, Octo would be ideal. With a few hundred lines of Octo code I proved that CHIP-8 was in fact complete enough to do the demo.  Then I had to think, how much RAM is in the VIP?  1K? 2K?  4K?  Can the demo be done in that?  I examined the VIP and found it had 2K installed (in sockets) and sockets for another 2K.  Scrounging around I found four 2114 memory ICs that I had bought to repair an ET-3400.  Well that would have to wait.  The VIP needed them now.  And it had waited patiently for almost 40 years to get them. So I installed the ICs and checked for basic functionality. The fact that the video still operated in the monitor was a good sign- because the video page is operated at the end of  memory- in the new RAM.

newram

Confident in the vastness of my newly augmented memory, I jumped back into writing code, expanding the program size with wild abandon.  I achieved a good level of functionality with about 3k bytes.

Here’s the CosmacCalc spreadsheet in all its glory.  Wow- five full rows of a sophisticated bill of material- resistors, capacitors, and transistors- with quantity and price, subtotals for each row, and a final summed total at the end.

cosmaccalcsheet

The code took form by first drawing the spreadsheet as hard coded text lines.  When it looked right, I broke up the lines into draw routines for each individual cell, allowing me to draw and redraw them at will.  Then I moved the numeric values into byte arrays and made those cells display from of the array values.  The nout (number out) routine converts a binary number to BCD digits using CHIP-8’s register-BCD-digit feature and then output the digits to the screen, suppressing leading zeros.  Then I added code to re-calculate the derived fields- the products of the quantity and price, and the sum of the line items.  CHIP-8 has no multiply support, so I implemented multiply with successive adds of course.  CHIP-8 only supports 8 bit math directly and I did not feel it was worth doing better.  The last functional part added was navigating around the spreadsheet and entering numbers into the count and price fields. This is where being able to redisplay a field at will was essential.  CHIP-8 writes video using XOR logic. Writing the same data twice clears the area written.  So successive pairs of cell writes produces a blinking cell.  This is used heavily in CosmacCalc for effect and for showing navigation around the sheet.  The original VisiCalc was said to support a demo mode. So I planned for a demo mode in CosmacCalc where it steps through the features essential to an interactive spreadsheet: rows and columns, cells and recalculation.  The demo screens were authored leveraging the display, navigate and recalculate routines written for the interactive mode. Also like VisiCalc on the Apple ][, the keyboard input is not interrupt driven so I similarly had to arrange for input polling around the code.  Consequently you have to hold key presses until the code gets around to noticing the action- and beeps for feedback.

Here’s how the demo operates:

The C key escapes out of the demo, putting it in interactive mode. Then you have up/down/left/right control with A/B/D/E.  You can navigate around the spreadsheet, and on the inner columns of quantity and price you can enter up to 2 digits and press F for Enter, whereupon the magical recalculation occurs. 
Holding down C puts you back into the demo which is really overkill because flicking the reset toggle switch start it over in demo.

Limited for sure, but it does show what a spreadsheet could have been all about on this platform.

With the essential features in place, I decided it was time to try it out on the real deal. Ultimately this requires combining the CHIP-8 op codes from Oct0 with the interpreter virtual machine, and loading that into the VIP.  One way to combing them would be to load the interpreter binary file and the CosmacCalc binary file into an emulator and then write out the memory footprint.  There is an added benefit to this- the two can be tested together to insure that everything is right.  So I chose that and found the Emma2 emulator up to the job.  The Cosmac VIP has support for program save and load from audio cassette. So the final piece I needed was a way to translate the binary memory image file to a wave file that would work in place of a cassette player.  So here’s a summary of my intended tool chain:

  1. Save the Octo code to a disk file.  I copied and pasted the Octo CHIP-8 ASCII code window contents to a text file and saved it.
  2. Convert ASCII HEX  format to binary bytes
  3. Combine the CHIP-8 interpreter and my CosmacCalc CHIP-8 op codes in Emma2.
  4. Test it; it should run just like in the Octo CHIP-8 emulator
  5. Save the entire 1802 memory range of 0000 to the page end of CosmacCalc out to a binary image, usually called a .ram file in Emma2 conventions.
  6. To insure the .ram file was correct, load this back into Emma2 and make sure it runs the same as in step 3.
  7. Run the .ram file through a converter to make a wav file.
  8. Load the .wav into Audacity and play it into the cassette read of the VIP

I needed to find or make the utilities for 2 and 7.  I really hoped #7 already existed.

Step #1:  I just used notepad. But watch out- you have save as “all files” or it puts a .txt at the end of the file name.

Step #2: I looked back through some C source code that I had done some simple file access with and hacked together this crude but working converter using Code::Blocks IDE (which I find to be a very nice no fuss IDE tool)

#include <stdio.h>
#include <unistd.h>

int main (int argc, char *argv[])
{
    unsigned int u;
    FILE *fpI, *fpO;
    int count = 0;

    if (argc == 3) {
        fpI = fopen (argv[1], "r");
        if (fpI == NULL) { printf ("Err file 1 Input"); return (-1);}
        fpO = fopen (argv[2], "wb+");
        if (fpO == NULL) { printf ("Err file 2 Output"); return (-1);}
        while (fscanf(fpI, "0x%2x, ", &u) == 1) {
            fputc(u, fpO);
            count += 1;
        }
        printf ("Count: \n%d\n", count);
        fclose (fpO);
        fclose (fpI);
    }
}

The converter is run with the text input file and binary output file name.
ie: c8h2b CosmacCalc.c8h CosmacCalc.c8b
My extension convention: c8h = CHIP-8 hex, and c8b = CHIP-8 binary.

Step #3 & 4:  Loading the .c8b binary into Emma2. The interpreter loads into memory locations 0000-01FF and the CosmacCalc CHIP-8 opcodes load into memory right after that. Here it is, loaded and running.

CosmaCalcInEmma2

Step #5:  save out the full memory image.  Hit Reset to run the “app”. That makes the Memory Access fields available.  Change the address fields to encompass the whole memory footprint: Set the START address to 0000 and modify the End address to end in FF   In the example below, the field previously had 0Axx where xx was the actual end of the binary image loaded.  I made it 0AFF so that my image includes data to the end of the page.  On the VIP the cassette load is told how many pages to expect and it expects data present to the end of the page.  In this case the number of pages is 11 pages specified as a hex B entry.  Hit the SAVE button and specify a file name as shown below.

CosmaCalcSave

Step #6: loading this back in and make sure it works. Close the emulator output window, set the RAM SW field to the .ram file, and clear the Chip-8/x/10 field, hit reset. Yep still runs.  The .ram file is ready for .wav conversion.

ramfiletest

Step #7:  .ram to .wav file.  For this I scoured the web and found a posting of a C program file just for the purpose at C2VIP.

/*

c2vip, Code to VIP Tape|Text, Version 0.2, Wed Jun 25 06:02:49 GMT 2014

Parts copyright (c) 2014 All Rights Reserved, Egan Ford (egan@sense.net)

THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
PARTICULAR PURPOSE.

Built on work by:
	* Paul Bourke (http://paulbourke.net/dataformats/audio/, AIFF and WAVE output code)
	* Malcolm Slaney and Ken Turkowski (Integer to IEEE 80-bit float code)

License:
	*  Do what you like, remember to credit all sources when using.

Description:
	This small utility will read COSMAC VIP binaries and output COSMAC VIP AIFF
	and WAV audio files for use with a cassette interface.

Features:
	*  Big and little-endian machine support.
		o  Little-endian tested.
	*  AIFF and WAVE output (both tested).
	*  Platforms tested:
		o  32-bit/64-bit x86 OS/X.
		o  32-bit/64-bit x86 Linux.
		o  32-bit x86 Windows/Cygwin.
		o  32-bit x86 Windows/MinGW.

Compile:
	OS/X:
		gcc -Wall -O -o c2vip c2vip.c
	Linux:
		gcc -Wall -O -o c2vip c2vip.c -lm
	Windows/Cygwin:
		gcc -Wall -O -o c2vip c2vip.c
	Windows/MinGW:
		PATH=C:\MinGW\bin;%PATH%
		gcc -Wall -O -static -o c2vip c2vip.c

Notes:
	*  Dropbox only supports .wav and .aiff (do not use .wave or .aif)

Not yet done:
	*  Test big-endian.
	*  gnuindent
    *  Redo malloc code in appendtone

Thinking about:
	*  Check for existing file and abort, or warn, or prompt.
	*  -q quiet option for Makefiles

Bugs:
	*  Probably

*/

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <unistd.h>
#include <string.h>
#include <math.h>
#include "c2vip.h"

#define ABS(x) (((x) < 0) ? -(x) : (x))

#define VERSION "Version 0.2"
#define OUTFILE argv[argc-1]
#define BINARY 0
#define MONITOR 1
#define AIFF 2
#define WAVE 3
#define DSK 4

#define WRITERBYTE(x) { \
	unsigned char wb_j, wb_temp=(x); \
	for(wb_j=0;wb_j<8;wb_j++) { \
		if(wb_temp & 1) \
			appendtone(&output,&outputlength,freq1,rate,0,1,&offset); \
		else \
			appendtone(&output,&outputlength,freq0,rate,0,1,&offset); \
		wb_temp>>=1; \
	} \
}

void usage();
char *getext(char *filename);
void appendtone(double **sound, long *length, int freq, int rate, double time, double cycles, int *offset);
void Write_AIFF(FILE * fptr, double *samples, long nsamples, int nfreq, int bits, double amp);
void Write_WAVE(FILE * fptr, double *samples, long nsamples, int nfreq, int bits, double amp);
void ConvertToIeeeExtended(double num, unsigned char *bytes);

int square = 0;

typedef struct seg {
	int start;
	int length;
	int codelength;
	unsigned char *data;
	char filename[256];
} segment;

int main(int argc, char **argv)
{
	FILE *ofp;
	double *output = NULL, amp=0.75;
	long outputlength=0;
	int i, j, c, outputtype, offset=0, fileoutput=1;
	int longmon=0, rate=48000, bits=8, freq0=2000, freq1=800;
	char *filetypes[] = {"binary","monitor","aiff","wave","disk"};
	char *ext;
	unsigned char pop, parity;
	unsigned int numseg = 0;
	segment *segments = NULL;

	opterr = 1;
	while((c = getopt(argc, argv, "vph?r:")) != -1)
		switch(c) {
			case 'v':		// version
				fprintf(stderr,"\n%s\n\n",VERSION);
				return 1;
				break;
			case 'p':		// stdout
				fileoutput = 0;
				break;
			case 'h':		// help
			case '?':
				usage();
				return 1;
			case 'r':		// override rate for -1/-2 only
				rate = atoi(optarg);
				break;
		}

	if(argc - optind < 1 + fileoutput) {
		usage();
		return 1;
	}

	// read intput files

	fprintf(stderr,"\n");
	for(i=optind;i<argc-fileoutput;i++) {
		unsigned char b, *data;
		int j, k, inputtype=BINARY;
		segment *tmp;
		FILE *ifp;

		if((tmp = realloc(segments, (numseg+1) * sizeof(segment))) == NULL) {
			fprintf(stderr,"could not allocate segment %d\n",numseg+1);
			abort();
		}
		segments = tmp;

		k=0;
		for(j=0;j<strlen(argv[i]);j++) {
			if(argv[i][j] == ',')
				break;
			segments[numseg].filename[k++]=argv[i][j];
		}
		segments[numseg].filename[k] = '\0';
		// TODO: store as basename, check for MINGW compat

/*
		if((ext = getext(segments[numseg].filename)) != NULL)
			if(strcmp(ext,"mon") == 0)
				inputtype = MONITOR;
*/

		if ((ifp = fopen(segments[numseg].filename, "rb")) == NULL) {
			fprintf(stderr,"Cannot read: %s\n\n",segments[numseg].filename);
			return 1;
		}

		fprintf(stderr,"Reading %s, type %s, segment %d, start: ",segments[numseg].filename,filetypes[inputtype],numseg+1);

		if((data = malloc(64*1024*sizeof(char))) == NULL) {
			fprintf(stderr,"could not allocate 64K data\n");
			abort();
		}

		if(inputtype == BINARY) {
			segments[numseg].start = 0;
			segments[numseg].length = 0;
			while(fread(&b, 1, 1, ifp) == 1)
				data[segments[numseg].length++]=b;

			segments[numseg].data = data;
			fprintf(stderr,"0x%04X, length: %d\n",segments[numseg].start,segments[numseg].length);
		}

		fclose(ifp);
		numseg++;
	}
	fprintf(stderr,"\n");

	if(fileoutput) {
		if((ext = getext(OUTFILE)) == NULL) {
			usage();
			return 1;
		}
		else {
			if(strcmp(ext,"aiff") == 0 || strcmp(ext,"aif") == 0)
				outputtype = AIFF;
			else if(strcmp(ext,"wave") == 0 || strcmp(ext,"wav") == 0)
				outputtype = WAVE;
			else if(strcmp(ext,"mon") == 0)
				outputtype = MONITOR;
			else {
				usage();
				return 1;
			}
		}
	}
	else {
		outputtype = MONITOR;
	}

	ofp=stdout;
	if(fileoutput) {
		if ((ofp = fopen(OUTFILE, "w")) == NULL) {
			fprintf(stderr,"\nCannot write: %s\n\n",OUTFILE);
			return 1;
		}
		fprintf(stderr,"Writing %s as %s formatted %s.\n\n",OUTFILE,"COSMAC VIP",filetypes[outputtype]);
	}
	else
		fprintf(stderr,"Writing %s as %s formatted %s.\n\n","STDOUT","COSMAC VIP",filetypes[outputtype]);

	if(outputtype == MONITOR) {
		int i, j, saddr;

		for(i=0;i<numseg;i++) {
			saddr = segments[i].start;
			fprintf(ofp,"%04X:", saddr);
			for(j=0;j<segments[i].length;j++) {
				fprintf(ofp," %02X", segments[i].data[j]);
				if(++saddr % (8+(24*longmon)) == 0 && j < segments[i].length - 1)
					fprintf(ofp,"\n%04X:",saddr);
			}
			fprintf(ofp,"\n");
		}

		fclose(ofp);
		return 0;
	}

	for(i=0;i<numseg;i++) {
		appendtone(&output,&outputlength,2000,rate,4.0,0,&offset);

		for(j=0;j<segments[i].length;j++) {
			// start bit
			appendtone(&output,&outputlength,freq1,rate,0,1,&offset);
			// data bits
			WRITERBYTE(segments[i].data[j]);
			// even parity
			pop = segments[i].data[j];
			parity = 0;
			for(;pop;parity=(parity==0))
				pop &= pop - 1;
			if(parity)
				appendtone(&output,&outputlength,freq1,rate,0,1,&offset);
			else
				appendtone(&output,&outputlength,freq0,rate,0,1,&offset);
		}
	}

	// append zero to zero out last wave
	appendtone(&output,&outputlength,0,rate,0,1,&offset);

	// 0.1 sec quiet to help some emulators
	appendtone(&output,&outputlength,0,rate,0.1,0,&offset);

	// 0.4 sec quiet to help some IIs
	// appendtone(&output,&outputlength,0,rate,0.4,0,&offset);

	// write it
	if(outputtype == AIFF)
		Write_AIFF(ofp,output,outputlength,rate,bits,amp);
	else if(outputtype == WAVE)
		Write_WAVE(ofp,output,outputlength,rate,bits,amp);

	fclose(ofp);
	return 0;
}

void appendtone(double **sound, long *length, int freq, int rate, double time, double cycles, int *offset)
{
	long i, n=time*rate;
	static long grow = 0;
	double *tmp = NULL;

	if(freq && cycles)
		n=cycles*rate/freq;

	if(n == 0)
		n=cycles;

/*
	if((tmp = (double *)realloc(*sound, (*length + n) * sizeof(double))) == NULL)
		abort();
	*sound = tmp;
*/

// new code for speed up Windows realloc
	if(*length + n > grow) {
		grow = *length + n + 10000000;
		if((tmp = (double *)realloc(*sound, (grow) * sizeof(double))) == NULL)
			abort();
		*sound = tmp;
	}

//tmp -> (*sound)
	if(square) {
		int j;

		if(freq)
			for (i = 0; i < n; i++) {
				for(j = 0;j < rate / freq / 2;j++)
					(*sound)[*length + i++] = 1;
				for(j = 0;j < rate / freq / 2;j++)
					(*sound)[*length + i++] = -1;
				i--;
			}
		else
			for (i = 0; i < n; i++)
				(*sound)[*length + i] = 0;
	}
	else
		for(i=0;i<n;i++)
			(*sound)[*length+i] = sin(2*M_PI*i*freq/rate + *offset*M_PI);

	if(cycles - (int)cycles == 0.5)
		*offset = (*offset == 0);

	*length += n;
}

char *getext(char *filename)
{
	char stack[256], *rval;
	int i, sp = 0;

	for(i=strlen(filename)-1;i>=0;i--) {
		if(filename[i] == '.')
			break;
		stack[sp++] = filename[i];
	}
	stack[sp] = '\0';

	if(sp == strlen(filename) || sp == 0)
		return(NULL);

	if((rval = (char *)malloc(sp * sizeof(char))) == NULL)
		; //do error code

	rval[sp] = '\0';
	for(i=0;i<sp+i;i++)
		rval[i] = stack[--sp];

	return(rval);
}

void usage()
{
	fprintf(stderr,"%s",usagetext);
}

// Code below from http://paulbourke.net/dataformats/audio/
/*
   Write an AIFF sound file
   Only do one channel, only support 16 bit.
   Supports sample frequencies of 11, 22, 44KHz (default).
   Little/big endian independent!
*/

// egan: changed code to support any Hz and 8 bit.

void Write_AIFF(FILE * fptr, double *samples, long nsamples, int nfreq, int bits, double amp)
{
	unsigned short v;
	int i;
	unsigned long totalsize;
	double themin, themax, scale, themid;
	unsigned char bit80[10];

	// Write the form chunk
	fprintf(fptr, "FORM");
	totalsize = 4 + 8 + 18 + 8 + (bits / 8) * nsamples + 8;
	fputc((totalsize & 0xff000000) >> 24, fptr);
	fputc((totalsize & 0x00ff0000) >> 16, fptr);
	fputc((totalsize & 0x0000ff00) >> 8, fptr);
	fputc((totalsize & 0x000000ff), fptr);
	fprintf(fptr, "AIFF");

	// Write the common chunk
	fprintf(fptr, "COMM");
	fputc(0, fptr);				// Size
	fputc(0, fptr);
	fputc(0, fptr);
	fputc(18, fptr);
	fputc(0, fptr);				// Channels = 1
	fputc(1, fptr);
	fputc((nsamples & 0xff000000) >> 24, fptr);	// Samples
	fputc((nsamples & 0x00ff0000) >> 16, fptr);
	fputc((nsamples & 0x0000ff00) >> 8, fptr);
	fputc((nsamples & 0x000000ff), fptr);
	fputc(0, fptr);				// Size = 16
	fputc(bits, fptr);

	ConvertToIeeeExtended(nfreq, bit80);
	for (i = 0; i < 10; i++)
		fputc(bit80[i], fptr);

	// Write the sound data chunk
	fprintf(fptr, "SSND");
	fputc((((bits / 8) * nsamples + 8) & 0xff000000) >> 24, fptr);	// Size
	fputc((((bits / 8) * nsamples + 8) & 0x00ff0000) >> 16, fptr);
	fputc((((bits / 8) * nsamples + 8) & 0x0000ff00) >> 8, fptr);
	fputc((((bits / 8) * nsamples + 8) & 0x000000ff), fptr);
	fputc(0, fptr);				// Offset
	fputc(0, fptr);
	fputc(0, fptr);
	fputc(0, fptr);
	fputc(0, fptr);				// Block
	fputc(0, fptr);
	fputc(0, fptr);
	fputc(0, fptr);

	// Find the range
	themin = samples[0];
	themax = themin;
	for (i = 1; i < nsamples; i++) {
		if (samples[i] > themax)
			themax = samples[i];
		if (samples[i] < themin)
			themin = samples[i];
	}
	if (themin >= themax) {
		themin -= 1;
		themax += 1;
	}
	themid = (themin + themax) / 2;
	themin -= themid;
	themax -= themid;
	if (ABS(themin) > ABS(themax))
		themax = ABS(themin);
//  scale = amp * 32760 / (themax);
	scale = amp * ((bits == 16) ? 32760 : 124) / (themax);

	// Write the data
	for (i = 0; i < nsamples; i++) {
		if (bits == 16) {
			v = (unsigned short) (scale * (samples[i] - themid));
			fputc((v & 0xff00) >> 8, fptr);
			fputc((v & 0x00ff), fptr);
		} else {
			v = (unsigned char) (scale * (samples[i] - themid));
			fputc(v, fptr);
		}
	}
}

/*
   Write an WAVE sound file
   Only do one channel, only support 16 bit.
   Supports any (reasonable) sample frequency
   Little/big endian independent!
*/

// egan: changed code to support 8 bit.

void Write_WAVE(FILE * fptr, double *samples, long nsamples, int nfreq, int bits, double amp)
{
	unsigned short v;
	int i;
	unsigned long totalsize, bytespersec;
	double themin, themax, scale, themid;

	// Write the form chunk
	fprintf(fptr, "RIFF");
	totalsize = (bits / 8) * nsamples + 36;
	fputc((totalsize & 0x000000ff), fptr);	// File size
	fputc((totalsize & 0x0000ff00) >> 8, fptr);
	fputc((totalsize & 0x00ff0000) >> 16, fptr);
	fputc((totalsize & 0xff000000) >> 24, fptr);
	fprintf(fptr, "WAVE");
	fprintf(fptr, "fmt ");		// fmt_ chunk
	fputc(16, fptr);			// Chunk size
	fputc(0, fptr);
	fputc(0, fptr);
	fputc(0, fptr);
	fputc(1, fptr);				// Format tag - uncompressed
	fputc(0, fptr);
	fputc(1, fptr);				// Channels
	fputc(0, fptr);
	fputc((nfreq & 0x000000ff), fptr);	// Sample frequency (Hz)
	fputc((nfreq & 0x0000ff00) >> 8, fptr);
	fputc((nfreq & 0x00ff0000) >> 16, fptr);
	fputc((nfreq & 0xff000000) >> 24, fptr);
	bytespersec = (bits / 8) * nfreq;
	fputc((bytespersec & 0x000000ff), fptr);	// Average bytes per second
	fputc((bytespersec & 0x0000ff00) >> 8, fptr);
	fputc((bytespersec & 0x00ff0000) >> 16, fptr);
	fputc((bytespersec & 0xff000000) >> 24, fptr);
	fputc((bits / 8), fptr);		// Block alignment
	fputc(0, fptr);
	fputc(bits, fptr);			// Bits per sample
	fputc(0, fptr);
	fprintf(fptr, "data");
	totalsize = (bits / 8) * nsamples;
	fputc((totalsize & 0x000000ff), fptr);	// Data size
	fputc((totalsize & 0x0000ff00) >> 8, fptr);
	fputc((totalsize & 0x00ff0000) >> 16, fptr);
	fputc((totalsize & 0xff000000) >> 24, fptr);

	// Find the range
	themin = samples[0];
	themax = themin;
	for (i = 1; i < nsamples; i++) {
		if (samples[i] > themax)
			themax = samples[i];
		if (samples[i] < themin)
			themin = samples[i];
	}
	if (themin >= themax) {
		themin -= 1;
		themax += 1;
	}
	themid = (themin + themax) / 2;
	themin -= themid;
	themax -= themid;
	if (ABS(themin) > ABS(themax))
		themax = ABS(themin);
//  scale = amp * 32760 / (themax);
	scale = amp * ((bits == 16) ? 32760 : 124) / (themax);

	// Write the data
	for (i = 0; i < nsamples; i++) {
		if (bits == 16) {
			v = (unsigned short) (scale * (samples[i] - themid));
			fputc((v & 0x00ff), fptr);
			fputc((v & 0xff00) >> 8, fptr);
		} else {
			v = (unsigned char) (scale * (samples[i] - themid));
			fputc(v + 0x80, fptr);
		}
	}
}

/*
 * C O N V E R T   T O   I E E E   E X T E N D E D
 */

/* Copyright (C) 1988-1991 Apple Computer, Inc.
 * All rights reserved.
 *
 * Machine-independent I/O routines for IEEE floating-point numbers.
 *
 * NaN's and infinities are converted to HUGE_VAL or HUGE, which
 * happens to be infinity on IEEE machines.  Unfortunately, it is
 * impossible to preserve NaN's in a machine-independent way.
 * Infinities are, however, preserved on IEEE machines.
 *
 * These routines have been tested on the following machines:
 *    Apple Macintosh, MPW 3.1 C compiler
 *    Apple Macintosh, THINK C compiler
 *    Silicon Graphics IRIS, MIPS compiler
 *    Cray X/MP and Y/MP
 *    Digital Equipment VAX
 *
 *
 * Implemented by Malcolm Slaney and Ken Turkowski.
 *
 * Malcolm Slaney contributions during 1988-1990 include big- and little-
 * endian file I/O, conversion to and from Motorola's extended 80-bit
 * floating-point format, and conversions to and from IEEE single-
 * precision floating-point format.
 *
 * In 1991, Ken Turkowski implemented the conversions to and from
 * IEEE double-precision format, added more precision to the extended
 * conversions, and accommodated conversions involving +/- infinity,
 * NaN's, and denormalized numbers.
 */

#ifndef HUGE_VAL
#define HUGE_VAL HUGE
#endif							/*HUGE_VAL */

#define FloatToUnsigned(f) ((unsigned long)(((long)(f - 2147483648.0)) + 2147483647L) + 1)

void ConvertToIeeeExtended(double num, unsigned char *bytes)
{
	int sign;
	int expon;
	double fMant, fsMant;
	unsigned long hiMant, loMant;

	if (num < 0) {
		sign = 0x8000;
		num *= -1;
	} else {
		sign = 0;
	}

	if (num == 0) {
		expon = 0;
		hiMant = 0;
		loMant = 0;
	} else {
		fMant = frexp(num, &expon);
		if ((expon > 16384) || !(fMant < 1)) {	/* Infinity or NaN */
			expon = sign | 0x7FFF;
			hiMant = 0;
			loMant = 0;			/* infinity */
		} else {				/* Finite */
			expon += 16382;
			if (expon < 0) {	/* denormalized */
				fMant = ldexp(fMant, expon);
				expon = 0;
			}
			expon |= sign;
			fMant = ldexp(fMant, 32);
			fsMant = floor(fMant);
			hiMant = FloatToUnsigned(fsMant);
			fMant = ldexp(fMant - fsMant, 32);
			fsMant = floor(fMant);
			loMant = FloatToUnsigned(fsMant);
		}
	}

	bytes[0] = expon >> 8;
	bytes[1] = expon;
	bytes[2] = hiMant >> 24;
	bytes[3] = hiMant >> 16;
	bytes[4] = hiMant >> 8;
	bytes[5] = hiMant;
	bytes[6] = loMant >> 24;
	bytes[7] = loMant >> 16;
	bytes[8] = loMant >> 8;
	bytes[9] = loMant;
}

I threw this code into Code::Blocks console project and it built without issue. I ran it in a command window, with the .ram file as input and specifying a .wav as output.

Step #8: The moment of truth- loading on a real machine.  I loaded the wave file into Audacity, set the playback level to a nominally high level and did the VIP cassette read command:  throw the toggle switch out of reset while holding the C button, then enter 0000 (start address), B (for cassette read) and B (for 11 blocks).  During a load the VIP’s screen goes black, and a load is finished if successful, the screen comes back and shows the ending address.
It did.  A flick of the reset switch starts a loaded program.  I flicked and it ran. In fact CosmacCalc ran the first time loaded.

With the end-run around the tool chain completed, I went back to the code and polished up the code.

Then it was off to VCFE. And It was a big hit.
displaySpreadSheetSpread

Finally, after the fact, I did a demo that shows and explains the whole thing.

https://www.youtube.com/watch?v=6KCUhjaCdjo

Here is the CosmacCalc loaded in Octo.  You can view the code and run it there just like I did.

The lesson of the development for this project is Octo is really remarkable for writing CHIP-8 programs.  Whatever can be done with Octo can be brought to any CHIP-8 environment- but most significantly it can be brought right to the real and original thing- the Cosmac VIP. 

My spreadsheet demo has nothing very notable internally, its implementation pretty simple and in many cases brute-forced.  At best it is a slightly novel a-typical use of CHIP-8.  There are much cleverer programs written in CHIP-8. The marvel was that I could bring this demo to the Cosmac VIP in just a few days, thanks to Octo.  Back in the day it would have been a really difficult job to write in straight CHIP-8 op codes.

So we are left to wonder if the little COSMAC and an interactive spreadsheet demo might have been able to change microcomputer history.   

Many credit the success of VisiCalc to the availability the Apple ][  with memory mapped video up to 80×25, 48K RAM memory capacity, and Woz’s Disk ][ floppy drive system – at a relatively low price.  The COSMAC would have needed a similar configuration to be a viable spreadsheet platform.  That’s a tall order.  It wasn’t until 1983 that perhaps the only similarly configured 1802 desktop system was introduced: the COMX-35.  For all of the valiant efforts of RCA and Joeseph Weisbecker, the 6502 had a huge system development lead over the 1802.

The Apple ][‘s available configuration in 1979 when VisiCalc was introduced was pivotal, but even more important was the visionary team that did the conceiving:  Dan Bricklin and Bob Frankston.  Without folks like that behind a new product concept, the effort is likely to flounder.

So the emergence of a popular1802 based desktop computer and CosmacCalc as its killer app was probably very unlikely, but its fun to consider “what if”.

Advertisements

5 Comments

Filed under COSMAC, VCFE 10.0, Vintage Bitage

5 responses to “CosmacCalc: The Cosmac VIP’s place in Spreadsheet History

  1. I believe you are incorrect in your statement: “[OCTO…] is written in java”. I’m pretty sure it is all written as a JavaScript web application. “Java” and “JavaScript” are two completely different animals that are only minimally related, and that latter may be (or absolutely is) a bit of an overstatement.

    Also, It’s capitalized as “Octo”, not “OCTO”, and “Java” should also be capitalized, for accuracy.

    Like

  2. This is the most wonderful thing I’ve ever read. Sometimes when I’m working across platforms I refer to the toolchain as a fantastic collection of stamps. Yours has far outdone my Towers of peril.

    And the outcome is very nice too!

    Liked by 1 person

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s