// // TapePRG.cpp // Clock Signal // // Created by Thomas Harte on 14/08/2016. // Copyright 2016 Thomas Harte. All rights reserved. // #include "TapePRG.hpp" /* 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. */ #include using namespace Storage::Tape; PRG::PRG(const std::string &file_name) : file_(file_name) { // 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; load_address_ = file_.get16le(); length_ = static_cast(file_.stats().st_size - 2); if (load_address_ + length_ >= 65536) throw ErrorBadFormat; } Storage::Tape::Tape::Pulse PRG::virtual_get_next_pulse() { // these are all microseconds per pole static const unsigned int leader_zero_length = 179; static const unsigned int zero_length = 169; static const unsigned int one_length = 247; static const unsigned int marker_length = 328; bit_phase_ = (bit_phase_+1)&3; if(!bit_phase_) get_next_output_token(); Tape::Pulse pulse; pulse.length.clock_rate = 1000000; pulse.type = (bit_phase_&1) ? Tape::Pulse::High : Tape::Pulse::Low; switch(output_token_) { case Leader: pulse.length.length = leader_zero_length; break; case Zero: pulse.length.length = (bit_phase_&2) ? one_length : zero_length; break; case One: pulse.length.length = (bit_phase_&2) ? zero_length : one_length; break; case WordMarker: pulse.length.length = (bit_phase_&2) ? one_length : marker_length; break; case EndOfBlock: pulse.length.length = (bit_phase_&2) ? zero_length : marker_length; break; case Silence: pulse.type = Tape::Pulse::Zero; pulse.length.length = 5000; break; } return pulse; } void PRG::virtual_reset() { bit_phase_ = 3; file_.seek(2, SEEK_SET); file_phase_ = FilePhaseLeadIn; phase_offset_ = 0; copy_mask_ = 0x80; } bool PRG::is_at_end() { return file_phase_ == FilePhaseAtEnd; } void PRG::get_next_output_token() { static const int block_length = 192; // not counting the checksum static const int countdown_bytes = 9; static const int leadin_length = 20000; static const int block_leadin_length = 5000; if(file_phase_ == FilePhaseHeaderDataGap || file_phase_ == FilePhaseAtEnd) { output_token_ = Silence; if(file_phase_ != FilePhaseAtEnd) file_phase_ = FilePhaseData; return; } // 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 if(file_phase_ == FilePhaseLeadIn || phase_offset_ < block_leadin_length) { output_token_ = Leader; phase_offset_++; if(file_phase_ == FilePhaseLeadIn && phase_offset_ == leadin_length) { phase_offset_ = 0; file_phase_ = (file_phase_ == FilePhaseLeadIn) ? FilePhaseHeader : FilePhaseData; } return; } // determine whether a new byte needs to be queued up int block_offset = phase_offset_ - block_leadin_length; int bit_offset = block_offset % 10; int byte_offset = block_offset / 10; phase_offset_++; if(!bit_offset && ( (file_phase_ == FilePhaseHeader && byte_offset == block_length + countdown_bytes + 1) || file_.eof() ) ) { output_token_ = EndOfBlock; phase_offset_ = 0; switch(file_phase_) { default: break; case FilePhaseHeader: copy_mask_ ^= 0x80; if(copy_mask_) file_phase_ = FilePhaseHeaderDataGap; break; case FilePhaseData: copy_mask_ ^= 0x80; file_.seek(2, SEEK_SET); if(copy_mask_) file_phase_ = FilePhaseAtEnd; break; } return; } if(bit_offset == 0) { // the first nine bytes are countdown; the high bit is set if this is a header if(byte_offset < countdown_bytes) { output_byte_ = static_cast(countdown_bytes - byte_offset) | copy_mask_; } else { if(file_phase_ == FilePhaseHeader) { if(byte_offset == countdown_bytes + block_length) { output_byte_ = check_digit_; } else { if(byte_offset == countdown_bytes) check_digit_ = 0; if(file_phase_ == 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; } } } } else { output_byte_ = file_.get8(); if(file_.eof()) { output_byte_ = check_digit_; } } check_digit_ ^= output_byte_; } } switch(bit_offset) { case 0: output_token_ = WordMarker; break; default: // i.e. 1–8 output_token_ = (output_byte_ & (1 << (bit_offset - 1))) ? One : Zero; break; case 9: { uint8_t parity = output_byte_; parity ^= (parity >> 4); parity ^= (parity >> 2); parity ^= (parity >> 1); output_token_ = (parity&1) ? Zero : One; } break; } }