2016-08-15 23:44:41 +00:00
|
|
|
|
//
|
|
|
|
|
// TapePRG.cpp
|
|
|
|
|
// Clock Signal
|
|
|
|
|
//
|
|
|
|
|
// Created by Thomas Harte on 14/08/2016.
|
|
|
|
|
// Copyright © 2016 Thomas Harte. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
#include "TapePRG.hpp"
|
|
|
|
|
|
2016-08-19 15:04:59 +00:00
|
|
|
|
/*
|
|
|
|
|
My interpretation of Commodore's tape format is such that a PRG is encoded as:
|
|
|
|
|
|
|
|
|
|
[long block of lead-in tone]
|
|
|
|
|
[short block of lead-in tone]
|
|
|
|
|
[count down][header; 192 bytes fixed length]
|
|
|
|
|
[short block of lead-in tone]
|
|
|
|
|
[count down][copy of header; 192 bytes fixed length]
|
|
|
|
|
[gap]
|
|
|
|
|
[short block of lead-in tone]
|
|
|
|
|
[count down][data; length as in file]
|
|
|
|
|
[short block of lead-in tone]
|
|
|
|
|
[count down][copy of data]
|
|
|
|
|
... and repeat ...
|
|
|
|
|
|
|
|
|
|
Individual bytes are composed of:
|
|
|
|
|
|
|
|
|
|
word marker
|
|
|
|
|
least significant bit
|
|
|
|
|
...
|
|
|
|
|
most significant bit
|
|
|
|
|
parity bit
|
|
|
|
|
|
|
|
|
|
Both the header and data blocks additionally end with an end-of-block marker.
|
|
|
|
|
|
|
|
|
|
Encoding is via square-wave cycles of four lengths, in ascending order: lead-in, zero, one, marker.
|
|
|
|
|
|
|
|
|
|
Lead-in tone is always just repetitions of the lead-in wave.
|
|
|
|
|
A word marker is a marker wave followed by a one wave.
|
|
|
|
|
An end-of-block marker is a marker wave followed by a zero wave.
|
|
|
|
|
A zero bit is a zero wave followed by a one wave.
|
|
|
|
|
A one bit is a one wave followed by a zero wave.
|
|
|
|
|
|
|
|
|
|
Parity is 1 if there are an even number of bits in the byte; 0 otherwise.
|
|
|
|
|
*/
|
|
|
|
|
|
2016-08-15 23:44:41 +00:00
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
|
2016-08-27 21:18:12 +00:00
|
|
|
|
using namespace Storage::Tape;
|
2016-08-15 23:44:41 +00:00
|
|
|
|
|
2016-08-27 21:09:45 +00:00
|
|
|
|
PRG::PRG(const char *file_name) : _file(nullptr), _bitPhase(3), _filePhase(FilePhaseLeadIn), _phaseOffset(0), _copy_mask(0x80)
|
2016-08-15 23:44:41 +00:00
|
|
|
|
{
|
|
|
|
|
struct stat file_stats;
|
|
|
|
|
stat(file_name, &file_stats);
|
|
|
|
|
|
|
|
|
|
// There's really no way to validate other than that if this file is larger than 64kb,
|
|
|
|
|
// of if load address + length > 65536 then it's broken.
|
|
|
|
|
if(file_stats.st_size >= 65538 || file_stats.st_size < 3)
|
|
|
|
|
throw ErrorBadFormat;
|
|
|
|
|
|
|
|
|
|
_file = fopen(file_name, "rb");
|
|
|
|
|
if(!_file) throw ErrorBadFormat;
|
|
|
|
|
|
|
|
|
|
_load_address = (uint16_t)fgetc(_file);
|
|
|
|
|
_load_address |= (uint16_t)fgetc(_file) << 8;
|
2016-08-16 02:40:05 +00:00
|
|
|
|
_length = (uint16_t)(file_stats.st_size - 2);
|
2016-08-15 23:44:41 +00:00
|
|
|
|
|
2016-08-16 02:40:05 +00:00
|
|
|
|
if (_load_address + _length >= 65536)
|
2016-08-15 23:44:41 +00:00
|
|
|
|
throw ErrorBadFormat;
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-27 21:09:45 +00:00
|
|
|
|
PRG::~PRG()
|
2016-08-15 23:44:41 +00:00
|
|
|
|
{
|
|
|
|
|
if(_file) fclose(_file);
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-27 21:09:45 +00:00
|
|
|
|
Storage::Tape::Tape::Pulse PRG::get_next_pulse()
|
2016-08-15 23:44:41 +00:00
|
|
|
|
{
|
2016-08-17 01:09:50 +00:00
|
|
|
|
// these are all microseconds per pole
|
2016-08-16 00:08:50 +00:00
|
|
|
|
static const unsigned int leader_zero_length = 179;
|
|
|
|
|
static const unsigned int zero_length = 169;
|
|
|
|
|
static const unsigned int one_length = 247;
|
2016-08-16 02:40:05 +00:00
|
|
|
|
static const unsigned int marker_length = 328;
|
2016-08-16 00:08:50 +00:00
|
|
|
|
|
|
|
|
|
_bitPhase = (_bitPhase+1)&3;
|
|
|
|
|
if(!_bitPhase) get_next_output_token();
|
|
|
|
|
|
2016-08-15 23:44:41 +00:00
|
|
|
|
Tape::Pulse pulse;
|
2016-08-19 14:58:42 +00:00
|
|
|
|
pulse.length.clock_rate = 1000000;
|
2016-08-27 21:09:45 +00:00
|
|
|
|
pulse.type = (_bitPhase&1) ? Tape::Pulse::High : Tape::Pulse::Low;
|
2016-08-16 00:08:50 +00:00
|
|
|
|
switch(_outputToken)
|
|
|
|
|
{
|
2016-08-16 02:40:05 +00:00
|
|
|
|
case Leader: pulse.length.length = leader_zero_length; break;
|
|
|
|
|
case Zero: pulse.length.length = (_bitPhase&2) ? one_length : zero_length; break;
|
|
|
|
|
case One: pulse.length.length = (_bitPhase&2) ? zero_length : one_length; break;
|
|
|
|
|
case WordMarker: pulse.length.length = (_bitPhase&2) ? one_length : marker_length; break;
|
|
|
|
|
case EndOfBlock: pulse.length.length = (_bitPhase&2) ? zero_length : marker_length; break;
|
2016-08-27 21:09:45 +00:00
|
|
|
|
case Silence: pulse.type = Tape::Pulse::Zero; pulse.length.length = 5000; break;
|
2016-08-16 00:08:50 +00:00
|
|
|
|
}
|
2016-08-15 23:44:41 +00:00
|
|
|
|
return pulse;
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-27 21:09:45 +00:00
|
|
|
|
void PRG::reset()
|
2016-08-15 23:44:41 +00:00
|
|
|
|
{
|
2016-08-16 00:08:50 +00:00
|
|
|
|
_bitPhase = 3;
|
|
|
|
|
fseek(_file, 2, SEEK_SET);
|
2016-08-16 02:10:53 +00:00
|
|
|
|
_filePhase = FilePhaseLeadIn;
|
|
|
|
|
_phaseOffset = 0;
|
2016-08-17 12:03:34 +00:00
|
|
|
|
_copy_mask = 0x80;
|
2016-08-16 00:08:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
2016-08-30 01:53:06 +00:00
|
|
|
|
bool PRG::is_at_end()
|
|
|
|
|
{
|
|
|
|
|
return _filePhase == FilePhaseAtEnd;
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-27 21:09:45 +00:00
|
|
|
|
void PRG::get_next_output_token()
|
2016-08-16 00:08:50 +00:00
|
|
|
|
{
|
2016-08-17 01:09:50 +00:00
|
|
|
|
static const int block_length = 192; // not counting the checksum
|
|
|
|
|
static const int countdown_bytes = 9;
|
2016-08-19 14:58:42 +00:00
|
|
|
|
static const int leadin_length = 20000;
|
|
|
|
|
static const int block_leadin_length = 5000;
|
|
|
|
|
|
2016-08-30 01:53:06 +00:00
|
|
|
|
if(_filePhase == FilePhaseHeaderDataGap || _filePhase == FilePhaseAtEnd)
|
2016-08-19 14:58:42 +00:00
|
|
|
|
{
|
|
|
|
|
_outputToken = Silence;
|
2016-08-30 01:53:06 +00:00
|
|
|
|
if(_filePhase != FilePhaseAtEnd) _filePhase = FilePhaseData;
|
2016-08-19 14:58:42 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
2016-08-17 01:09:50 +00:00
|
|
|
|
|
2016-08-16 02:10:53 +00:00
|
|
|
|
// the lead-in is 20,000 instances of the lead-in pair; every other phase begins with 5000
|
|
|
|
|
// before doing whatever it should be doing
|
2016-08-19 14:58:42 +00:00
|
|
|
|
if(_filePhase == FilePhaseLeadIn || _phaseOffset < block_leadin_length)
|
2016-08-16 02:10:53 +00:00
|
|
|
|
{
|
|
|
|
|
_outputToken = Leader;
|
|
|
|
|
_phaseOffset++;
|
2016-08-19 14:58:42 +00:00
|
|
|
|
if(_filePhase == FilePhaseLeadIn && _phaseOffset == leadin_length)
|
2016-08-16 02:10:53 +00:00
|
|
|
|
{
|
|
|
|
|
_phaseOffset = 0;
|
2016-08-17 12:03:34 +00:00
|
|
|
|
_filePhase = (_filePhase == FilePhaseLeadIn) ? FilePhaseHeader : FilePhaseData;
|
2016-08-16 02:10:53 +00:00
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// determine whether a new byte needs to be queued up
|
2016-08-19 14:58:42 +00:00
|
|
|
|
int block_offset = _phaseOffset - block_leadin_length;
|
2016-08-16 02:10:53 +00:00
|
|
|
|
int bit_offset = block_offset % 10;
|
|
|
|
|
int byte_offset = block_offset / 10;
|
2016-08-17 01:09:50 +00:00
|
|
|
|
_phaseOffset++;
|
2016-08-16 02:10:53 +00:00
|
|
|
|
|
2016-08-19 14:58:42 +00:00
|
|
|
|
if(!bit_offset &&
|
|
|
|
|
(
|
|
|
|
|
(_filePhase == FilePhaseHeader && byte_offset == block_length + countdown_bytes + 1) ||
|
|
|
|
|
feof(_file)
|
|
|
|
|
)
|
|
|
|
|
)
|
2016-08-16 02:40:05 +00:00
|
|
|
|
{
|
|
|
|
|
_outputToken = EndOfBlock;
|
2016-08-17 02:17:08 +00:00
|
|
|
|
_phaseOffset = 0;
|
2016-08-17 12:03:34 +00:00
|
|
|
|
|
|
|
|
|
switch(_filePhase)
|
2016-08-17 02:17:08 +00:00
|
|
|
|
{
|
2016-08-17 12:03:34 +00:00
|
|
|
|
default: break;
|
|
|
|
|
case FilePhaseHeader:
|
|
|
|
|
_copy_mask ^= 0x80;
|
|
|
|
|
if(_copy_mask) _filePhase = FilePhaseHeaderDataGap;
|
|
|
|
|
break;
|
|
|
|
|
case FilePhaseData:
|
2016-08-19 14:58:42 +00:00
|
|
|
|
_copy_mask ^= 0x80;
|
|
|
|
|
fseek(_file, 2, SEEK_SET);
|
|
|
|
|
if(_copy_mask) reset();
|
2016-08-17 12:03:34 +00:00
|
|
|
|
break;
|
2016-08-17 02:17:08 +00:00
|
|
|
|
}
|
2016-08-16 02:40:05 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-16 02:10:53 +00:00
|
|
|
|
if(bit_offset == 0)
|
|
|
|
|
{
|
|
|
|
|
// the first nine bytes are countdown; the high bit is set if this is a header
|
2016-08-17 01:09:50 +00:00
|
|
|
|
if(byte_offset < countdown_bytes)
|
2016-08-16 02:10:53 +00:00
|
|
|
|
{
|
2016-08-17 12:03:34 +00:00
|
|
|
|
_output_byte = (uint8_t)(countdown_bytes - byte_offset) | _copy_mask;
|
2016-08-16 02:40:05 +00:00
|
|
|
|
}
|
2016-08-16 02:10:53 +00:00
|
|
|
|
else
|
|
|
|
|
{
|
2016-08-16 02:40:05 +00:00
|
|
|
|
if(_filePhase == FilePhaseHeader)
|
|
|
|
|
{
|
2016-08-19 14:58:42 +00:00
|
|
|
|
if(byte_offset == countdown_bytes + block_length)
|
2016-08-16 02:40:05 +00:00
|
|
|
|
{
|
2016-08-19 14:58:42 +00:00
|
|
|
|
_output_byte = _check_digit;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if(byte_offset == countdown_bytes) _check_digit = 0;
|
|
|
|
|
if(_filePhase == FilePhaseHeader)
|
|
|
|
|
{
|
|
|
|
|
switch(byte_offset - countdown_bytes)
|
|
|
|
|
{
|
|
|
|
|
case 0: _output_byte = 0x03; break;
|
|
|
|
|
case 1: _output_byte = _load_address & 0xff; break;
|
|
|
|
|
case 2: _output_byte = (_load_address >> 8)&0xff; break;
|
|
|
|
|
case 3: _output_byte = (_load_address + _length) & 0xff; break;
|
|
|
|
|
case 4: _output_byte = ((_load_address + _length) >> 8) & 0xff; break;
|
|
|
|
|
|
|
|
|
|
case 5: _output_byte = 0x50; break; // P
|
|
|
|
|
case 6: _output_byte = 0x52; break; // R
|
|
|
|
|
case 7: _output_byte = 0x47; break; // G
|
|
|
|
|
default:
|
|
|
|
|
_output_byte = 0x20;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-08-16 02:40:05 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_output_byte = (uint8_t)fgetc(_file);
|
2016-08-19 14:58:42 +00:00
|
|
|
|
if(feof(_file))
|
|
|
|
|
{
|
|
|
|
|
_output_byte = _check_digit;
|
|
|
|
|
}
|
2016-08-16 02:40:05 +00:00
|
|
|
|
}
|
2016-08-16 02:44:36 +00:00
|
|
|
|
|
|
|
|
|
_check_digit ^= _output_byte;
|
2016-08-16 02:10:53 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch(bit_offset)
|
|
|
|
|
{
|
|
|
|
|
case 0:
|
|
|
|
|
_outputToken = WordMarker;
|
|
|
|
|
break;
|
2016-08-17 01:09:50 +00:00
|
|
|
|
default: // i.e. 1–8
|
2016-08-16 02:10:53 +00:00
|
|
|
|
_outputToken = (_output_byte & (1 << (bit_offset - 1))) ? One : Zero;
|
|
|
|
|
break;
|
2016-08-17 01:09:50 +00:00
|
|
|
|
case 9:
|
2016-08-16 02:10:53 +00:00
|
|
|
|
{
|
2016-08-17 01:41:09 +00:00
|
|
|
|
uint8_t parity = _output_byte;
|
2016-08-16 02:10:53 +00:00
|
|
|
|
parity ^= (parity >> 4);
|
|
|
|
|
parity ^= (parity >> 2);
|
|
|
|
|
parity ^= (parity >> 1);
|
2016-08-17 01:41:09 +00:00
|
|
|
|
_outputToken = (parity&1) ? Zero : One;
|
2016-08-16 02:10:53 +00:00
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2016-08-15 23:44:41 +00:00
|
|
|
|
}
|