Kjetil's Information Center: A Blog About My Projects

Epson HX-20 Micro-Cassette Data Recovery

With the help of an Arduino and a Linux host PC I was able to recover some programs from Epson HX-20 micro-cassette tapes. The Arduino interfaces directly with the HX-20 micro-cassette player and send/receives data over the UART to Linux. The signals are at 5V TTL level so I used the 5V 16MHz Arduino Pro Mini. The motors and logic on the micro-cassette player also operates at 5V so I used the regulated VCC from the Arduino directly for this, split into two wires.

Connection table:

|---------|----------|---------|
| Signal  | HX-20 MC | Arduino |
| Name    | Pin      | Pin     |
|---------|----------|---------|
| RD/WE   | 1        | 2       |
| CNT/HSW | 2        | 3       |
| CLK     | 4        | 4       |
| CMMND   | 5        | 5       |
| PWSW    | 6        | 6       |
| VE      | 7        | VCC     |
| GND     | 11       | GND     |
| VL      | 12       | VCC     |
|---------|----------|---------|
          


Photo of the setup:

Arduino connected to HX-20 micro-cassette player


I had first intended to use an interrupt on the Arduino to read the cassette data pulses, but figured out it was easier to just poll this at a fixed rate and post-process it on Linux. Here is the Arduino sketch:

const byte PIN_RD  = 2;
const byte PIN_HSW = 3;
const byte PIN_CLK = 4;
const byte PIN_CMD = 5;
const byte PIN_PW  = 6;

bool stop_on_hsw0 = false;
bool stop_on_hsw1 = false;

void setup() {
  Serial.begin(115200);
  pinMode(PIN_RD, INPUT_PULLUP);
  pinMode(PIN_HSW, INPUT_PULLUP);
  pinMode(PIN_CLK, OUTPUT);
  pinMode(PIN_CMD, OUTPUT);
  pinMode(PIN_PW, OUTPUT);
}

void loop() {
  char bits;

  /* Accept commands to set ouputs: */
  if (Serial.available() > 0) {
    switch (Serial.read()) {
    case 'd':
      digitalWrite(PIN_CMD, 0);
      break;
    case 'D':
      digitalWrite(PIN_CMD, 1);
      break;
    case 'c':
      digitalWrite(PIN_CLK, 0);
      break;
    case 'C':
      digitalWrite(PIN_CLK, 1);
      break;
    case 'p':
      digitalWrite(PIN_PW, 0);
      break;
    case 'P':
      digitalWrite(PIN_PW, 1);
      break;
    case 's':
      stop_on_hsw0 = true;
      break;
    case 'S':
      stop_on_hsw1 = true;
      break;
    }
  }

  /* Special modes to cut power quickly on HSW inputs: */
  if (stop_on_hsw0) {
    if ((digitalRead(PIN_CLK) > 0) && (digitalRead(PIN_HSW) == 0)) {
      digitalWrite(PIN_PW, 0);
      stop_on_hsw0 = false;
    }
  }
  if (stop_on_hsw1) {
    if ((digitalRead(PIN_CLK) > 0) && (digitalRead(PIN_HSW) > 0)) {
      digitalWrite(PIN_PW, 0);
      stop_on_hsw1 = false;
    }
  }

  /* Encode three bits to send back, which can be read as ASCII: */
  bits = 0b01110000;
  if (digitalRead(PIN_CLK) > 0) {
    bits |= 0b001;
  }
  if (digitalRead(PIN_RD) > 0) {
    bits |= 0b010;
  }
  if (digitalRead(PIN_HSW) > 0) {
    bits |= 0b100;
  }
  Serial.print(bits);
}
          


The Arduino sketch provides a simple command API on the UART RX to set specific outputs while simultaneously streaming the state of the inputs (and clock) on the UART TX. Here is the counterpart Linux code to handle the UART:

#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <termios.h>
#include <errno.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>
#include <ctype.h>

#define TTY_DEVICE "/dev/ttyUSB0"
#define TTY_SPEED 115200

#define CMD_STOP    0x00  /* Operation stop */
#define CMD_REW     0x0A  /* Rewind */
#define CMD_PLAY    0x01  /* Data stop */
#define CMD_FF      0x11  /* Fast forward */
#define CMD_REC     0x81  /* Data read */
#define CMD_BRAKE   0x18  /* Capstan motor brake */
#define CMD_HLD     0x20  /* Head load/unload */
#define CMD_HBRAKE  0x40  /* Head motor brake */

static int tty_fd = -1;
static int quit = 0;
static bool head_switch = false;
static bool playing = false;

static void *read_thread(void *arg)
{
  (void)arg;
  int result;
  uint8_t c;

  while (! quit) {
    result = read(tty_fd, &c, 1);
    if (result == 1) {
      fprintf(stderr, "\r[%c]", isprint(c) ? c : '?');
      if ((c & 1) == 1) {
        /* Decode head switch state: */
        head_switch = (c >> 2) & 1;
      } else {
        /* Dump cassette read data: */
        if (playing) {
          fprintf(stdout, "%c", 0x30 + ((c >> 1) & 1));
        }
      }
    }
  }

  return ((void *)0);
}

static void send_command(uint8_t cmd)
{
  int i;
  uint8_t c;

  for (i = 7; i >= 0; i--) {
    c = ((cmd >> i) & 1) ? 'D' : 'd';
    write(tty_fd, &c, 1); /* Data */
    c = 'C';
    write(tty_fd, &c, 1); /* Clock High */

    if (cmd == CMD_HLD && i == 0) {
      /* Keep the clock high after issuing HLD command in order to be able
         to properly read the HSW signal and determine head position. */
      break;
    }
    c = 'c';
    write(tty_fd, &c, 1); /* Clock Low */
  }
}

static void clock_control(bool on)
{
  uint8_t c;
  if (on) {
    c = 'C';
  } else {
    c = 'c';
  }
  write(tty_fd, &c, 1);
}

static void power_control(bool on)
{
  uint8_t c;
  if (on) {
    c = 'P';
  } else {
    c = 'p';
  }
  write(tty_fd, &c, 1);
}

static void stop_on_hsw(int value)
{
  uint8_t c;
  if (value == 1) {
    c = 'S';
  } else {
    c = 's';
  }
  write(tty_fd, &c, 1);
}

int main(void)
{
  int c;
  int result;
  int timeout;
  struct termios tio;
  pthread_t tid;

  tty_fd = open(TTY_DEVICE, O_RDWR | O_NOCTTY);
  if (tty_fd == -1) {
    fprintf(stderr, "open() failed with errno: %d\n", errno);
    return 1;
  }

  cfmakeraw(&tio);
  cfsetospeed(&tio, B115200);

  result = ioctl(tty_fd, TCSETS, &tio);
  if (result == -1) {
    fprintf(stderr, "ioctl() failed with errno: %d\n", errno);
    close(tty_fd);
    return 1;
  }

  pthread_create(&tid, NULL, read_thread, NULL);

  /* Make sure power is off: */
  power_control(0);

  /* Make sure command shift register is cleared: */
  send_command(CMD_STOP);

  while ((c = fgetc(stdin)) != EOF) {
    switch (c) {
    case '1': /* Head unload (by sensor) */
      send_command(CMD_HLD);
      power_control(1);
      stop_on_hsw(0);
      timeout = 1000;
      while (head_switch == 1) {
        usleep(1000);
        timeout--;
        if (timeout <= 0) {
          break;
        }
      }
      clock_control(0);
      power_control(0);
      send_command(CMD_HBRAKE);
      usleep(10000);
      send_command(CMD_STOP);
      break;

    case '2': /* Head load (by sensor) */
      send_command(CMD_HLD);
      power_control(1);
      stop_on_hsw(1);
      timeout = 1000;
      while (head_switch == 0) {
        usleep(1000);
        timeout--;
        if (timeout <= 0) {
          break;
        }
      }
      clock_control(0);
      power_control(0);
      send_command(CMD_HBRAKE);
      usleep(10000);
      send_command(CMD_STOP);
      break;

    case '3': /* Play */
      playing = true;
      power_control(0);
      send_command(CMD_PLAY);
      power_control(1);
      break;

    case '4': /* Stop */
      playing = false;
      power_control(0);
      send_command(CMD_BRAKE);
      usleep(10000);
      send_command(CMD_STOP);
      break;

    case '5': /* Rewind */
      power_control(0);
      send_command(CMD_REW);
      power_control(1);
      break;

    case 'q':
      quit = 1;
      break;
    }

    if (quit) {
      break;
    }
  }

  pthread_join(tid, NULL);
  close(tty_fd);
  return 0;
}
          


After playing back a micro-cassette tape you will end up with a text file (redirected from stdout of course) with a lot of 1s and 0s. I made some smaller C programs to convert this over to cleaned up WAV files in several stages.

The first program converts the raw stdout dump, which is actually frequency modulated signal levels, into the proper 1s and 0s by looking at the duration:

#include <stdio.h>
#include <stdbool.h>

int main(void)
{
  int c;
  int n;
  int len_high;
  int len_low;
  bool prev;

  n = 0;
  while ((c = fgetc(stdin)) != EOF) {
    if (c == '0') {
      if (prev == false) {
        if (len_high + len_low > 6) {
          fprintf(stdout, "1");
        } else {
          fprintf(stdout, "0");
        }

        /* Inject newlines for easy splicing during debugging. */
        n++;
        if (n % 80 == 0) {
          fprintf(stdout, "\n");
        }

        prev = true;
        len_high = 0;
      } else {
        len_high++;
      }

    } else if (c == '1') {
      if (prev == true) {
        prev = false;
        len_low = 0;
      } else {
        len_low++;
      }
    }
  }

  return 0;
}
          


The second program converts the 1s and 0s back into frequency modulated high/low levels that match the more standard 44100 Hz sampling rate:

#include <stdio.h>

#define LEN_0 158
#define LEN_1 316

int main(void)
{
  int c;
  int i;

  while ((c = fgetc(stdin)) != EOF) {
    if (c == '0') {
      for (i = 0; i < LEN_0; i++) {
        fprintf(stdout, "H");
      }
      for (i = 0; i < LEN_0; i++) {
        fprintf(stdout, "L");
      }

    } else if (c == '1') {
      for (i = 0; i < LEN_1; i++) {
        fprintf(stdout, "H");
      }
      for (i = 0; i < LEN_1; i++) {
        fprintf(stdout, "L");
      }
    }
  }

  return 0;
}
          


The third program can take this high/low stream at 44100Hz and make a WAV file out of it:

#include <stdio.h>
#include <stdint.h>
#include <limits.h>

#define SAMPLE_RATE_IN  612900 /* HX-20 MPU Speed */
#define SAMPLE_RATE_OUT 44100

#define SAMPLE_MAX 16777216

static uint8_t sample[SAMPLE_MAX];

int main(void)
{
  int c;
  unsigned int input_count;
  uint32_t sample_count;
  uint32_t chunk_size;
  uint32_t sample_rate;
  uint32_t subchunk2_size;

  input_count = 0;
  sample_count = 0;
  while ((c = fgetc(stdin)) != EOF) {
    if (input_count % (SAMPLE_RATE_IN / SAMPLE_RATE_OUT) == 0) {
      if (c == 'H') {
        sample[sample_count] = UINT8_MAX;
        sample_count++;
      } else if (c == 'L') {
        sample[sample_count] = 0;
        sample_count++;
      }
      if (sample_count >= SAMPLE_MAX) {
        break;
      }
    }
    input_count++;
  }

  subchunk2_size = sample_count;
  chunk_size = subchunk2_size + 36;
  sample_rate = SAMPLE_RATE_OUT;

  fwrite("RIFF", sizeof(char), 4, stdout);
  fwrite(&chunk_size, sizeof(uint32_t), 1, stdout);
  fwrite("WAVE", sizeof(char), 4, stdout);
  fwrite("fmt ", sizeof(char), 4, stdout);
  fwrite("\x10\x00\x00\x00", sizeof(char), 4, stdout); /* Subchunk1Size = 16 */
  fwrite("\x01\x00", sizeof(char), 2, stdout); /* AudioFormat = 1 = PCM */
  fwrite("\x01\x00", sizeof(char), 2, stdout); /* Channels = 1 = Mono */
  fwrite(&sample_rate, sizeof(uint32_t), 1, stdout);
  fwrite(&sample_rate, sizeof(uint32_t), 1, stdout); /* ByteRate */
  fwrite("\x01\x00", sizeof(char), 2, stdout); /* BlockAlign = 1 */
  fwrite("\x08\x00", sizeof(char), 2, stdout); /* BitsPerSample = 8 */
  fwrite("data", sizeof(char), 4, stdout);
  fwrite(&subchunk2_size, sizeof(uint32_t), 1, stdout);

  for (uint32_t i = 0; i < sample_count; i++) {
    fwrite(&sample[i], sizeof(uint8_t), 1, stdout);
  }

  return 0;
}
          


The resulting WAV file can be played back to a real Epson HX-20 using it's external cassette interface input, but another choice is to use my hex20 emulator to read the WAV file and dump it to an ASCII text file.

I found the following four programs of interest on some of my tapes:
* "COVER ME" (Bruce Springsteen's "Cover Me" supposedly.) (YouTube)
* "MOZART" (Mozart's "Eine Kleine Nachtmusik" and maybe something else.) (YouTube)
* "KLOKKE" (Norwegian clock program with stop-watch and alarm functionality.)
* "GEO" (Norwegian geography game, run it with a Scandinavian character set.)

Those programs can be downloaded in ASCII format here.

Topic: Scripts and Code, by Kjetil @ 13/10-2023, Article Link