Jump to content
IGNORED

EduEncode - a library for encoding Atari Educational System tapes!


Recommended Posts

Hello everyone, I am now officially announcing the first version of 'eduencode', a C library that can encode WAV files that are compatible with the Atari Educational System Master Cartridge (CXL4001). It provides a complete API for virtually every feature of the Atari Educational System, including:

 

* ASCII text output

* highlighted ASCII text output

* Color settings for border, background, and hilight colors

* writing individual bytes for control codes if desired.

* clearing the screen

* introducing pauses

* indicating the correct answer for a following query

* query to ask for an answer. (1, 2, or 3)

* etc.

 

All of the functions are immediate, and cause the given data to be appended to the file automatically.

 

The WAV file produced is stereo, with the data in the right channel, as needed, and can either be directly used, or fed into a program such as Audacity, Soundforge, or others, for voice over processing in the left channel.

 

It is intended that this library be used, to create a user friendly authoring tool, so that others can make educational system tapes, or for it to be used as an archaeological tool, as part of a curation to document how the Atari Educational System works.

 

I am attaching a ZIP file containing the current revision of the code, and will post up to github, later this week. I am also pasting a copy of the library source in this thread, as well as the test harness, so everyone can have a look.

 

eduencode.c - the implementation

 

 

/* eduencode.c - Routines to create Atari Educational System tapes */
/* Author: Thomas Cherryhomes <thom.cherryhomes@gmail.com>         */
 
#include <math.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
 
#include "eduencode.h"
 
#define FALSE 0
#define TRUE 1
#define SAMPLE_RATE 44100            /* Output Sample Rate */
#define HZ_MARK 5327                 /* Mark Frequency */
#define HZ_SPACE 3995                /* Space Frequency */
#define SECS_LEADER 4                /* Leader time in seconds */
#define NUM_CHANNELS 1               /* output channels - 1 */
#define BUFFER_SIZE 74               /* buffer size for a single 600 baud bit transition. */
#define AMPLITUDE 16384              /* amplitude for samples, right now at half volume. */
#define LEFT_CHANNEL 0               /* Lower 16 bits = Left Channel */
#define RIGHT_CHANNEL 16             /* Upper 16 bits = right channel */
#define BYTES_PER_SAMPLE 4           /* 16-bit samples, 2 channels */
#define BITS_PER_SAMPLE 16           /* Therefore 32 bits per total sample */
#define BYTE_RATE 176400             /* how many bytes per second, data rate */
#define HEADER_LENGTH 36             /* The RIFF WAVE file header is 36 bytes total */
 
int32_t edu_data_len = 0;
 
int edu_write_leader(FILE* fp)
{
  int i;
  float amplitude = AMPLITUDE;
  float freq_hz = HZ_MARK;
  float phase=0;
  int buffer_size = SAMPLE_RATE * NUM_CHANNELS * SECS_LEADER;
  float freq_radians_per_sample = freq_hz*2*M_PI/SAMPLE_RATE;
  int32_t buffer[buffer_size];
 
  for (i=0;i<buffer_size;i++)
    {
      if (phase > M_PI)
phase = (phase - (2*M_PI));
      phase += freq_radians_per_sample;
      buffer[i] = (int32_t)(amplitude*sin(phase)) << RIGHT_CHANNEL; /* << 16 ensures right channel. */
    }
 
  if (fwrite(&buffer,sizeof(int32_t),buffer_size,fp) != buffer_size)
    {
      return FALSE;
    }
 
  edu_data_len=edu_data_len+(sizeof(int32_t)*buffer_size);
 
  return TRUE;
}
 
int edu_write_wav_header(FILE* fp, int32_t len)
{
  int32_t total_len = HEADER_LENGTH+len;
  int32_t subchunk_size = 16;
  int16_t format = 1;
  int16_t num_channels = 2;
  int32_t sample_rate = SAMPLE_RATE;
  int32_t byte_rate = BYTE_RATE;
  int16_t bytes_per_sample = BYTES_PER_SAMPLE;
  int16_t bits_per_sample = BITS_PER_SAMPLE;
 
  fwrite("RIFF",1,4,fp);
  fwrite(&total_len,sizeof(int32_t),1,fp); // Write total length
  fwrite("WAVE",1,4,fp); // Write WAVE subpart
  fwrite("fmt ",1,4,fp); // write fmt subpart
  fwrite(&subchunk_size,sizeof(int32_t),1,fp); // subchunk size is 16.
  fwrite(&format,sizeof(int16_t),1,fp); // PCM is format 1
  fwrite(&num_channels,sizeof(int16_t),1,fp); // num channels = 2
  fwrite(&sample_rate,sizeof(int32_t),1,fp); // 44100hz sample rate
  fwrite(&byte_rate,sizeof(int32_t),1,fp); // byte rate is 176400
  fwrite(&bytes_per_sample,sizeof(int16_t),1,fp); // 2 bytes per sample
  fwrite(&bits_per_sample,sizeof(int16_t),1,fp); // 32 bits per sample.
  fwrite("data",1,4,fp); // data chunk
  fwrite(&len,sizeof(int32_t),1,fp); // data length
  return TRUE; // FIXME TODO: Come back here and do some fucking error handling.
}
 
FILE *edu_open_file(const char* filename)
{
  FILE* fp;
  fp = fopen(filename,"wb");
 
  edu_data_len=0;
  if (edu_write_wav_header(fp,0) == FALSE)
    return NULL;
 
  if (edu_write_leader(fp) == FALSE)
    return NULL;
 
  return fp;
}
 
int edu_close_file(FILE* fp)
{
  fseek(fp,0,SEEK_SET);
  edu_write_wav_header(fp,edu_data_len); // Update header now that we have total length.
  fseek(fp,0,SEEK_END);
  fclose(fp);
  return TRUE;
}
 
int edu_write_baud(FILE* fp, float freq_hz)
{
  int i;
  float amplitude = AMPLITUDE;
  float phase=0;
  int buffer_size = BUFFER_SIZE;
  float freq_radians_per_sample = freq_hz*2*M_PI/SAMPLE_RATE;
  int32_t buffer[buffer_size];
 
  for (i=0;i<buffer_size;i++)
    {
      if (phase > M_PI)
phase = (phase - (2*M_PI));
      phase += freq_radians_per_sample;
      buffer[i] = (int32_t)(amplitude*sin(phase)) << RIGHT_CHANNEL; /* << 16 ensures right channel. */
    }
 
  if (fwrite(&buffer,sizeof(int32_t),buffer_size,fp) != buffer_size)
    {
      return FALSE;
    }
 
  edu_data_len=edu_data_len+(sizeof(int32_t)*buffer_size);
 
  return TRUE;  
}
 
int edu_write_mark(FILE* fp)
{
  return edu_write_baud(fp,HZ_MARK);
}
 
int edu_write_space(FILE* fp)
{
  return edu_write_baud(fp,HZ_SPACE);
}
 
int edu_write_start_bit(FILE* fp)
{
  return edu_write_space(fp);
}
 
int edu_write_stop_bit(FILE* fp)
{
  return edu_write_mark(fp);
}
 
int edu_write_byte(FILE* fp, unsigned char b)
{
  int i=0;
  if (edu_write_start_bit(fp) == FALSE)
    return FALSE;
 
  b = b ^ 0xFF; // XOR the byte with $FF, invert all bits as specified.
  
  for (i=0; i<8; i++)
    {
      if (b & (1<<i))
{
  if (edu_write_mark(fp) == FALSE)
    return FALSE;
}
      else
{
  if (edu_write_space(fp) == FALSE)
    return FALSE;
}
    }
 
  if (edu_write_stop_bit(fp) == FALSE)
    return FALSE;
 
  return TRUE;
}
 
int edu_write_string(FILE* fp, const char* text)
{
  int buffer_size = strlen(text);
  int i=0;
 
  for (i=0; i<buffer_size; i++)
    {
      if (edu_write_byte(fp,text[i]) == FALSE)
return FALSE;
      if (edu_write_byte(fp,0) == FALSE)
return FALSE;
    }
  return TRUE;
}
 
int edu_write_hilight_string(FILE* fp, const char* text)
{
  int buffer_size = strlen(text);
  int i=0;
 
  for (i=0; i<buffer_size; i++)
    {
      if (edu_write_byte(fp,(text[i] | 0x80)) == FALSE)
return FALSE;
      if (edu_write_byte(fp,0) == FALSE)
return FALSE;
    }
  
  return TRUE;  
}
 
int edu_write_pause(FILE* fp, int ms)
{
  int num_of_nulls = ms * .06;
  int i=0;
 
  for (i=0; i<num_of_nulls;i++)
    {
      if (edu_write_byte(fp,0) == FALSE)
{
  return FALSE;
}
    }
  return TRUE;
}
 
int edu_write_clear(FILE* fp)
{
  return edu_write_byte(fp,0x03);
}
 
int edu_write_hilight_blink_off(FILE* fp)
{
  return edu_write_byte(fp,0x02);
}
 
int edu_write_hilight_blink_on(FILE *fp)
{
  return edu_write_byte(fp,0x05);
}
 
int edu_write_background_color(FILE *fp, BackgroundColor backgroundColor)
{
  return edu_write_byte(fp,backgroundColor);
}
 
int edu_write_border_color(FILE *fp, BorderColor borderColor)
{
  return edu_write_byte(fp,borderColor);
}
 
int edu_write_hilight_color(FILE *fp, HilightColor hilightColor)
{
  return edu_write_byte(fp,hilightColor);
}
 
int edu_write_colors(FILE *fp, BackgroundColor backgroundColor, BorderColor borderColor, HilightColor hilightColor)
{
  edu_write_background_color(fp,backgroundColor);
  edu_write_border_color(fp,borderColor);
  edu_write_hilight_color(fp,hilightColor);
  return TRUE;
}
 
int edu_write_correct_answer(FILE *fp, CorrectAnswer answer)
{
  return edu_write_byte(fp,answer);
}
 
int edu_write_wait_for_answer(FILE *fp)
{
  return edu_write_byte(fp,0x84);
}

 

eduencode.h - the public API

 

 

/* eduencode.c - Routines to create Atari Educational System tapes */
/* Author: Thomas Cherryhomes <thom.cherryhomes@gmail.com>         */
 
#include <stdio.h>
 
enum BackgroundColorEnum { BACKGROUND_DARK_RED=0x19, BACKGROUND_MAGENTA=0x1A, BACKGROUND_RED=0x1b, BACKGROUND_BROWN=0x1c, BACKGROUND_GREEN=0x1d, BACKGROUND_BLUE=0x1e };
typedef enum BackgroundColorEnum BackgroundColor;
 
enum BorderColorEnum { BORDER_DARK_RED=0x11, BORDER_MAGENTA=0x12, BORDER_RED=0x13, BORDER_BROWN=0x14, BORDER_GREEN=0x15, BORDER_BLUE=0x16 };
typedef enum BorderColorEnum BorderColor;
 
enum HilightColorEnum { HILIGHT_DARK_RED=0x99, HILIGHT_MAGENTA=0x9A, HILIGHT_RED=0x9B, HILIGHT_BROWN=0x9C, HILIGHT_GREEN=0x9d, HILIGHT_BLUE=0x9E };
typedef enum HilightColorEnum HilightColor;
 
enum CorrectAnswerEnum { ANSWER_1=0x08, ANSWER_2=0x09, ANSWER_3=0x0A };
typedef enum CorrectAnswerEnum CorrectAnswer;
 
FILE *edu_open_file(const char* filename);
int edu_close_file(FILE* fp);
int edu_write_byte(FILE* fp, unsigned char b);
int edu_write_string(FILE* fp, const char* text);
int edu_write_hilight_string(FILE* fp, const char* text);
int edu_write_pause(FILE* fp, int ms);
int edu_write_clear(FILE* fp);
int edu_write_hilight_blink_off(FILE* fp);
int edu_write_hilight_blink_on(FILE *fp);
int edu_write_background_color(FILE *fp, BackgroundColor backgroundColor);
int edu_write_border_color(FILE *fp, BorderColor borderColor);
int edu_write_hilight_color(FILE *fp, HilightColor hilightColor);
int edu_write_colors(FILE *fp, BackgroundColor backgroundColor, BorderColor borderColor, HilightColor hilightColor);
int edu_write_correct_answer(FILE *fp, CorrectAnswer answer);
int edu_write_wait_for_answer(FILE *fp);

 

and test_eduencode.c, the test harness, to show how it works:

 

 

/* Test Harness */
 
#include "eduencode.h"
#include <stdio.h>
 
int main(int argc, char* argv[])
{
  FILE* fp = edu_open_file("test_eduencode.wav");
 
  edu_write_string(fp,"------------------------------\r\r");
  edu_write_string(fp,"This is an example tape being\r");
  edu_write_string(fp,"Written by the ");
  edu_write_hilight_string(fp," eduencode \r");
  edu_write_string(fp,"library; Released as free\r");
  edu_write_string(fp,"software under the protections\r");
  edu_write_string(fp,"of v3 of the");
  edu_write_hilight_string(fp," GNU Public License ");
  edu_write_string(fp,"\r\r");
  edu_write_hilight_color(fp,HILIGHT_DARK_RED);
  edu_write_pause(fp,600);
  edu_write_hilight_color(fp,HILIGHT_RED);
  edu_write_pause(fp,600);
  edu_write_hilight_color(fp,HILIGHT_BROWN);
  edu_write_pause(fp,600);
  edu_write_hilight_color(fp,HILIGHT_GREEN);
  edu_write_pause(fp,600);
  edu_write_pause(fp,6000);
 
  edu_write_colors(fp,BACKGROUND_GREEN,BORDER_GREEN,HILIGHT_RED);
 
  edu_write_clear(fp);
 
  edu_write_string(fp,"This code can be used to both\r");
  edu_write_string(fp,"create new Atari Educational\r");
  edu_write_string(fp,"System tapes,");
  edu_write_pause(fp,400);
  edu_write_string(fp,"as well as\r");
  edu_write_string(fp,"Provide a historical study of\r");
  edu_write_string(fp,"The tape format, ");
  edu_write_pause(fp,350);
  edu_write_string(fp,"which, ");
  edu_write_pause(fp,150);
  edu_write_string(fp,"along\r");
  edu_write_string(fp,"with the documentation of the\r");
  edu_write_string(fp,"Master Cartridge, and its \r");
  edu_write_string(fp,"disassembly,");
  edu_write_pause(fp,450);
  edu_write_string(fp," serve as a \r");
  edu_write_string(fp,"complete curated exhibit of\r");
  edu_write_string(fp,"this form of computer based\r");
  edu_write_string(fp,"education.\r");
  edu_write_pause(fp,6000);
 
  edu_write_colors(fp,BACKGROUND_BROWN,BORDER_BROWN,HILIGHT_MAGENTA);
  edu_write_clear(fp);
 
  edu_write_hilight_string(fp," eduencode ");
  edu_write_string(fp," outputs a\r");
  edu_write_string(fp,"standard WAV file, which can be\r");
  edu_write_string(fp,"read by a variety of audio pro-\r");
  edu_write_string(fp,"grams to be able to be written\r");
  edu_write_string(fp,"to a cassette tape, and certain\r");
  edu_write_string(fp,"emulators, such as Altirra, can\r");
  edu_write_string(fp,"play these tapes, in emulation.\r");
  edu_write_pause(fp,8000);
 
  edu_write_colors(fp,BACKGROUND_BROWN,BORDER_BROWN,HILIGHT_GREEN);
  edu_write_clear(fp);
 
  edu_write_string(fp,"This tape was written using the ");
  edu_write_hilight_string(fp," eduencode ");
  edu_write_string(fp," library\r");
  edu_write_string(fp," functions, such as:\r\r");
  edu_write_hilight_string(fp,"-------------------------------\r");
  edu_write_hilight_string(fp,"| edu_write_string(fp,'Hi');  |\r");
  edu_write_hilight_string(fp,"| edu_write_pause(fp,5000);   |\r");
  edu_write_hilight_string(fp,"| edu_write_correct_answer... |\r");
  edu_write_hilight_string(fp,"| ...                         |\r");
  edu_write_hilight_string(fp,"-------------------------------\r");  
  edu_write_pause(fp,7000);
 
  edu_write_colors(fp,BACKGROUND_BLUE,BORDER_GREEN,HILIGHT_RED);
  edu_write_clear(fp);
 
  edu_write_string(fp,"What license is ");
  edu_write_hilight_string(fp," eduencode ");
  edu_write_string(fp,"\rlicensed");
  edu_write_string(fp," under?");
  edu_write_correct_answer(fp,ANSWER_3);
  edu_write_string(fp,"\r\r");
  edu_write_string(fp,"  GPL         GPL          GPL\r");
  edu_write_string(fp,"  v.1         v.2          v.3\r");
  edu_write_wait_for_answer(fp);
  edu_write_colors(fp,BACKGROUND_GREEN,BORDER_GREEN,HILIGHT_RED);
  edu_write_string(fp,"------------------------------\r\r");
  edu_write_string(fp,"  Yes, that's correct.");
  edu_write_pause(fp,3000);
 
  edu_write_clear(fp);
 
  edu_write_string(fp,"Thank you for testing this. The\r");
  edu_write_string(fp,"author also wishes to thank\r");
  edu_write_string(fp,"AtariAge, and the Atari\r");
  edu_write_string(fp,"Community for their\r");
  edu_write_string(fp,"encouragement and support on\r"); 
  edu_write_string(fp,"this project.\r\r\r");
 
  edu_write_pause(fp,5000);
 
  edu_write_string(fp,"       end of tape.");
 
  edu_close_file(fp);
  return 0;
}

 

A video showing it all in action, is here:

 

 

Still more to do... now, it's documenting everything...

 

Any questions? Enjoy.

-Thom

eduencode.zip

  • Like 12
Link to comment
Share on other sites

I seem to be getting some bit corruption:

 

post-21021-0-62466500-1431497379_thumb.pngpost-21021-0-04473600-1431497527_thumb.png

 

Any ideas what might be causing this? I'm using Altirra 2.60 same as you showed in the video.

 

I do notice that my WAV file is slightly shorter than yours but all of the C source files are the same size. Perhaps there's some compiler-dependent thing going on? I'm using GCC under cygwin:

 

~/z/eduencode $ make
cc    -c -o eduencode.o eduencode.c
ar ru libeduencode.a eduencode.o
ar: creating libeduencode.a
ranlib libeduencode.a
cc    -c -o test-eduencode.o test-eduencode.c
cc -o test-eduencode test-eduencode.o -I. -L. -leduencode
~/z/eduencode $ ./test-eduencode.exe
~/z/eduencode $ ls -ln
total 14965
-rw-r--r-- 1 1000 513      657 May 12 16:34 Makefile
-rw-r--r-- 1 1000 513     6772 May 12 16:27 eduencode.c
-rw-r--r-- 1 1000 513     1684 May 12 16:25 eduencode.h
-rw-r--r-- 1 1000 513     5439 May 13 01:16 eduencode.o
-rw-r--r-- 1 1000 513     6138 May 13 01:16 libeduencode.a
-rw-r--r-- 1 1000 513     4275 May 12 17:28 test-eduencode.c
-rwxr-xr-x 1 1000 513    71486 May 13 01:16 test-eduencode.exe
-rw-r--r-- 1 1000 513     5731 May 13 01:16 test-eduencode.o
-rw-r--r-- 1 1000 513 15200764 May 13 01:16 test_eduencode.wav
~/z/eduencode $ gcc --version
gcc (GCC) 4.9.2
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Link to comment
Share on other sites

@phaeron As a quick test, I moved the phase variable out of edu_write_baud and made it global and now I don't have any corruption. So I think your diagnosis is correct. Thanks!

 

@tschak909 I still don't understand why I'm getting a WAV file from test-eduencode.c that's exactly 41440 bytes smaller than yours (about 117ms?). Maybe you changed a delay amount between making the video and before creating the zip file? Though I've been watching and pausing the video and comparing it with the source code in your zip file and I can't see any obvious differences.

  • Like 1
Link to comment
Share on other sites

basically just moving phase out into global space? probably should rename it to __phase, then...

 

Great catch, @phaeron. Now that I think about it, that should have been obvious...but that is the very first FSK modulator i've ever written, sorta just...guessed at it.

 

@Xuel thanks for testing! Now I just need to find some time to write a real authoring tool. I want to make at least one complete set of tapes.

 

-Thom

  • Like 1
Link to comment
Share on other sites

An Update from my side:

 

Now that the archaeology of the master cartridge disassembly and forensic analysis of the tape format is completed, and the encoder functions, I am currently writing up a book detailing the entire "dig", and will be releasing it to the public once complete.

 

It will consist of three major parts:

 

* Introduction

* Disassembling the Cartridge

* Forensic Analysis of the Cassette Format

 

The focus on this particular book is very much a "post mortem" of the Atari Educational System, with the intent of the book being viewed as a proper historical text deconstructing one of the earliest examples of both multimedia and computer based training.

(and yes, the eduencode encoder will be printed in the book, in its entirety, along with a complete explanation of the code)

I will then take the content of the book, and at the very least, take the reference bits and place onto AtariWiki, as this is the most logical place for it.

-Thom

  • Like 4
Link to comment
Share on other sites

Hey guys, so...

 

Roughly about 2am the other night, I had a crazy idea, to reimagine Strong Bad Email #45 - Techno, as an Edu System cassette :D :)

 

Here's the result

 

Awesome idea to show the possibilties. Laughed my a.. off.

  • Like 1
Link to comment
Share on other sites

Thom,

 

That's some nice piece of software! Thank you for your hard work on analysing the data format.

 

I took the liberty of rewriting your library to use liba8cas, as it's an ideal candidate for that. liba8cas is the core of my tape conversion software, and it contains the FSK ecoder/decoder and can output both WAV and CAS files - therefore eduencode can now output them, too!

 

basically just moving phase out into global space? probably should rename it to __phase, then...

Using global variables in a library is generally a bad idea - it makes the library non-reentrant and/or not thread-safe.

eduencode-a8cas.zip

  • Like 4
Link to comment
Share on other sites

Kr0tki,

 

Thank you for the port, and the compliments. :) Can .cas files also handle the left channel audio track of a dual track tape?

 

I did it to WAV, because I am publishing the code alongside the book I am writing, and wanted the code to be completely self contained, without any dependencies, and I wanted to be able to take the resulting files into an editing suite to drop in voiceover tracks. :)

 

-Thom

Link to comment
Share on other sites

Has _anyone_ recorded output to tape and tried this with a real setup? I _really_ want to know if I need to handle timing marks more aggressively.

 

-Thom

I'm recording it now to see if it works. Let you know what my results are soon.

 

Allan

  • Like 1
Link to comment
Share on other sites

I tried it tonight a few times with no luck. I got the recording on the tape and I hear the voices/recording but I just get garbage characters and random color changes (I think). I'm not sure if I am making the recording right or not. I am going to put one of the Atari releases on a blank cassette and see if that works. Than I will know if it's my recording that is the problem.

 

Allan

Link to comment
Share on other sites

I tried making a tape of a known working Atari tape and I see I need to play with the inputs a bit. Will try again tomorrow to get an original tape working, then I will know what to record at and go from there.

 

Allan

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...