diff --git a/Storage/Tape/Formats/CAS.cpp b/Storage/Tape/Formats/CAS.cpp index edce11dd2..6ea0b037a 100644 --- a/Storage/Tape/Formats/CAS.cpp +++ b/Storage/Tape/Formats/CAS.cpp @@ -8,21 +8,197 @@ #include "CAS.hpp" +#include +#include + using namespace Storage::Tape; -CAS::CAS(const char *file_name) : - file_(file_name) { +namespace { + const uint8_t header_signature[8] = {0x1f, 0xa6, 0xde, 0xba, 0xcc, 0x13, 0x7d, 0x74}; +} + +CAS::CAS(const char *file_name) { + Storage::FileHolder file(file_name); + uint8_t lookahead[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + + // Get the first header. + get_next(file, lookahead, 8); + if(std::memcmp(lookahead, header_signature, sizeof(header_signature))) throw ErrorNotCAS; + + File *active_file = nullptr; + while(!file.eof()) { + // Just found a header, so flush the lookahead. + get_next(file, lookahead, 8); + + // If no file is active, create one, as this must be an identification block. + if(!active_file) { + // Determine the new file type. + Block type; + switch(lookahead[0]) { + case 0xd3: type = Block::CSAVE; break; + case 0xd0: type = Block::BSAVE; break; + case 0xea: type = Block::ASCII; break; + + // This implies something has gone wrong with parsing. + default: throw ErrorNotCAS; + } + + // Set the type and feed in the initial data. + files_.emplace_back(); + active_file = &files_.back(); + active_file->type = type; + } + + // Add a new chunk for the new incoming data. + active_file->chunks.emplace_back(); + + // Keep going until another header arrives or the file ends. + while(std::memcmp(lookahead, header_signature, sizeof(header_signature)) && !file.eof()) { + active_file->chunks.back().push_back(lookahead[0]); + get_next(file, lookahead, 1); + } + + // If the file ended, flush the lookahead. + if(file.eof()) { + for(int index = 0; index < 8; index++) + active_file->chunks.back().push_back(lookahead[index]); + } + + switch(active_file->type) { + case Block::ASCII: + // ASCII files have as many chunks as necessary, the final one being back filled + // with 0x1a. + if(active_file->chunks.size() >= 2) { + std::vector &last_chunk = active_file->chunks.back(); + if(last_chunk.back() == 0x1a) + active_file = nullptr; + } + break; + + default: + // CSAVE and BSAVE files have exactly two chunks, the second being the data. + if(active_file->chunks.size() == 2) + active_file = nullptr; + break; + } + } +} + +/*! + Treating @c buffer as a sliding lookahead, shifts it @c quantity elements to the left and + populates the new empty area to the right from @c file. +*/ +void CAS::get_next(Storage::FileHolder &file, uint8_t (&buffer)[8], std::size_t quantity) { + assert(quantity <= 8); + + if(quantity < 8) + std::memmove(buffer, &buffer[quantity], 8 - quantity); + + while(quantity--) { + buffer[7 - quantity] = file.get8(); + } } bool CAS::is_at_end() { - return true; + return phase_ == Phase::EndOfFile; } void CAS::virtual_reset() { - + phase_ = Phase::Header; + file_pointer_ = 0; + chunk_pointer_ = 0; + distance_into_phase_ = 0; + distance_into_bit_ = 0; } Tape::Pulse CAS::virtual_get_next_pulse() { - Pulse empty_pulse; - return empty_pulse; + Pulse pulse; + pulse.length.clock_rate = 9600; + // Clock rate is four times the baud rate (of 2400), because the quickest thing that might need + // to be communicated is a '1', which is two cycles at the baud rate, i.e. four events: + // high, low, high, low. + + // If this is a gap, then that terminates a file. If this is already the end + // of the file then perpetual gaps await. + if(phase_ == Phase::Gap || phase_ == Phase::EndOfFile) { + pulse.length.length = pulse.length.clock_rate; + pulse.type = Pulse::Type::Zero; + + if(phase_ == Phase::Gap) { + phase_ = Phase::Header; + chunk_pointer_ = 0; + distance_into_phase_ = 0; + } + + return pulse; + } + + // Determine which bit is now forthcoming. + int bit = 1; + + switch(phase_) { + default: break; + + case Phase::Header: { + // In the header, all bits are 1s, so let the default value stand. Just check whether the + // header is ended and, if so, move on to bytes. + distance_into_bit_++; + if(distance_into_bit_ == 2) { + distance_into_phase_++; + distance_into_bit_ = 0; + + // This code always produces a 2400 baud signal; so use the appropriate Red Book-supplied + // constants to check whether the header has come to an end. + if(distance_into_phase_ == (chunk_pointer_ ? 7936 : 31744)) { + phase_ = Phase::Bytes; + distance_into_phase_ = 0; + distance_into_bit_ = 0; + } + } + } break; + + case Phase::Bytes: { + // Provide bits with a single '0' start bit and two '1' stop bits. + uint8_t byte_value = files_[file_pointer_].chunks[chunk_pointer_][distance_into_phase_ / 11]; + int bit_offset = distance_into_phase_ % 11; + switch(bit_offset) { + case 0: bit = 0; break; + default: bit = (byte_value >> (bit_offset - 1)) & 1; break; + case 9: + case 10: bit = 1; break; + } + + // Lots of branches below, to the effect that: + // + // if bit is finished, and if all bytes in chunk have been posted then: + // + // - if this is the final chunk in the file then, if there are further files switch to a gap. + // Otherwise note end of file. + // + // - otherwise, roll onto the next header. + // + distance_into_bit_++; + if(distance_into_bit_ == (bit ? 4 : 2)) { + distance_into_bit_ = 0; + distance_into_phase_++; + if(distance_into_phase_ == files_[file_pointer_].chunks[chunk_pointer_].size() * 11) { + distance_into_phase_ = 0; + chunk_pointer_++; + if(chunk_pointer_ == files_[file_pointer_].chunks.size()) { + chunk_pointer_ = 0; + file_pointer_++; + phase_ = (file_pointer_ == files_.size()) ? Phase::EndOfFile : Phase::Gap; + } else { + phase_ = Phase::Header; + } + } + } + } break; + } + + // A '1' is encoded with twice the frequency of a '0'. + pulse.length.length = static_cast(2 - bit); + pulse.type = (distance_into_bit_ & 1) ? Pulse::Type::High : Pulse::Type::Low; + + return pulse; } diff --git a/Storage/Tape/Formats/CAS.hpp b/Storage/Tape/Formats/CAS.hpp index 4b998e2b9..beaca5843 100644 --- a/Storage/Tape/Formats/CAS.hpp +++ b/Storage/Tape/Formats/CAS.hpp @@ -12,6 +12,9 @@ #include "../Tape.hpp" #include "../../FileHolder.hpp" +#include +#include + namespace Storage { namespace Tape { @@ -35,10 +38,35 @@ class CAS: public Tape { bool is_at_end(); private: - Storage::FileHolder file_; - void virtual_reset(); Pulse virtual_get_next_pulse(); + + // Helper for populating the file list, below. + void get_next(Storage::FileHolder &file, uint8_t (&buffer)[8], std::size_t quantity); + + // Storage for the array of files to transcribe into audio. + enum class Block { + BSAVE, + CSAVE, + ASCII + }; + struct File { + Block type; + std::vector> chunks; + }; + std::vector files_; + + // Tracker for active state within the file list. + std::size_t file_pointer_ = 0; + std::size_t chunk_pointer_ = 0; + enum class Phase { + Header, + Bytes, + Gap, + EndOfFile + } phase_ = Phase::Header; + std::size_t distance_into_phase_ = 0; + std::size_t distance_into_bit_ = 0; }; } diff --git a/Storage/Tape/Tape.cpp b/Storage/Tape/Tape.cpp index 0d5ce1504..6722f6575 100644 --- a/Storage/Tape/Tape.cpp +++ b/Storage/Tape/Tape.cpp @@ -89,8 +89,7 @@ void TapePlayer::get_next_pulse() { if(tape_) { current_pulse_ = tape_->get_next_pulse(); if(tape_->is_at_end()) update_sleep_observer(); - } - else { + } else { current_pulse_.length.length = 1; current_pulse_.length.clock_rate = 1; current_pulse_.type = Tape::Pulse::Zero;