// // Commodore.cpp // Clock Signal // // Created by Thomas Harte on 06/11/2016. // Copyright 2016 Thomas Harte. All rights reserved. // #include "Commodore.hpp" #include #include "Storage/Data/Commodore.hpp" using namespace Storage::Tape::Commodore; Parser::Parser(const TargetPlatform::Type target_platform) : Storage::Tape::PulseClassificationParser(), target_platform_(target_platform) {} /*! Advances to the next block on the tape, treating it as a header, then consumes, parses, and returns it. Returns @c nullptr if any wave-encoding level errors are encountered. */ std::unique_ptr
Parser::get_next_header(Storage::Tape::TapeSerialiser &serialiser) { return duplicate_match
( get_next_header_body(serialiser, true), get_next_header_body(serialiser, false) ); } /*! Advances to the next block on the tape, treating it as data, then consumes, parses, and returns it. Returns @c nullptr if any wave-encoding level errors are encountered. */ std::unique_ptr Parser::get_next_data(Storage::Tape::TapeSerialiser &serialiser) { return duplicate_match( get_next_data_body(serialiser, true), get_next_data_body(serialiser, false) ); } /*! Template for the logic in selecting which of two copies of something to consider authoritative, including setting the duplicate_matched flag. */ template std::unique_ptr Parser::duplicate_match( std::unique_ptr first_copy, std::unique_ptr second_copy ) { // If only one copy was parsed successfully, return it. if(!first_copy) return second_copy; if(!second_copy) return first_copy; // If no copies were second_copy, return nullptr. if(!first_copy && !second_copy) return nullptr; // Return either one with a correct check digit, doing a comparison with the other. std::unique_ptr *copy_to_return = &first_copy; if(!first_copy->parity_was_valid && second_copy->parity_was_valid) copy_to_return = &second_copy; (*copy_to_return)->duplicate_matched = true; if(first_copy->data.size() != second_copy->data.size()) (*copy_to_return)->duplicate_matched = false; else (*copy_to_return)->duplicate_matched = std::equal(first_copy->data.begin(), first_copy->data.end(), second_copy->data.begin()); return std::move(*copy_to_return); } std::unique_ptr
Parser::get_next_header_body( Storage::Tape::TapeSerialiser &serialiser, const bool is_original ) { auto header = std::make_unique
(); reset_error_flag(); // find and proceed beyond lead-in tone proceed_to_symbol(serialiser, SymbolType::LeadIn); // look for landing zone proceed_to_landing_zone(serialiser, is_original); reset_parity_byte(); // get header type const uint8_t header_type = get_next_byte(serialiser); switch(header_type) { default: header->type = Header::Unknown; break; case 0x01: header->type = Header::RelocatableProgram; break; case 0x02: header->type = Header::DataBlock; break; case 0x03: header->type = Header::NonRelocatableProgram; break; case 0x04: header->type = Header::DataSequenceHeader; break; case 0x05: header->type = Header::EndOfTape; break; } // grab rest of data header->data.reserve(191); for(std::size_t c = 0; c < 191; c++) { header->data.push_back(get_next_byte(serialiser)); } const uint8_t parity_byte = get_parity_byte(); header->parity_was_valid = get_next_byte(serialiser) == parity_byte; // parse if this is not pure data if(header->type != Header::DataBlock) { header->starting_address = uint16_t(header->data[0] | (header->data[1] << 8)); header->ending_address = uint16_t(header->data[2] | (header->data[3] << 8)); for(std::size_t c = 0; c < 16; c++) { header->raw_name.push_back(header->data[4 + c]); } header->name = Storage::Data::Commodore::petscii_from_bytes(&header->raw_name[0], 16, false); } if(get_error_flag()) return nullptr; return header; } uint8_t Header::type_descriptor() const { switch(type) { default: return 0xff; case Header::RelocatableProgram: return 0x01; case Header::DataBlock: return 0x02; case Header::NonRelocatableProgram: return 0x03; case Header::DataSequenceHeader: return 0x04; case Header::EndOfTape: return 0x05; } } void Header::serialise(uint8_t *const target, const uint16_t length) const { target[0] = type_descriptor(); const auto bytes_to_copy = std::min(size_t(length), data.size()); std::copy_n(data.begin(), bytes_to_copy, target); } std::unique_ptr Parser::get_next_data_body(Storage::Tape::TapeSerialiser &serialiser, const bool is_original) { auto data = std::make_unique(); reset_error_flag(); // find and proceed beyond lead-in tone to the next landing zone proceed_to_symbol(serialiser, SymbolType::LeadIn); proceed_to_landing_zone(serialiser, is_original); reset_parity_byte(); // accumulate until the next non-word marker is hit while(!serialiser.is_at_end()) { const SymbolType start_symbol = get_next_symbol(serialiser); if(start_symbol != SymbolType::Word) break; data->data.push_back(get_next_byte_contents(serialiser)); } // the above has read the parity byte to the end of the data; if it matched the calculated parity it'll now be zero data->parity_was_valid = !data->data.empty() && !get_parity_byte(); data->duplicate_matched = false; // remove the captured parity if(!data->data.empty()) data->data.erase(data->data.end()-1); if(get_error_flag()) return nullptr; return data; } /*! Finds and completes the next landing zone. */ void Parser::proceed_to_landing_zone(Storage::Tape::TapeSerialiser &serialiser, const bool is_original) { std::array landing_zone{0, 0, 0, 0, 0, 0, 0, 0, 0}; while(!serialiser.is_at_end()) { std::copy(landing_zone.begin() + 1, landing_zone.end(), landing_zone.begin()); landing_zone[8] = get_next_byte(serialiser); bool is_landing_zone = true; for(size_t c = 0; c < 9; c++) { if(landing_zone[c] != ((is_original ? 0x80 : 0x00) | 0x9) - c) { is_landing_zone = false; break; } } if(is_landing_zone) break; } } /*! Swallows the next byte; sets the error flag if it is not equal to @c value. */ void Parser::expect_byte(Storage::Tape::TapeSerialiser &serialiser, uint8_t value) { const uint8_t next_byte = get_next_byte(serialiser); if(next_byte != value) set_error_flag(); } void Parser::reset_parity_byte() { parity_byte_ = 0; } uint8_t Parser::get_parity_byte() const { return parity_byte_; } void Parser::add_parity_byte(const uint8_t byte) { parity_byte_ ^= byte; } /*! Proceeds to the next word marker then returns the result of @c get_next_byte_contents. */ uint8_t Parser::get_next_byte(Storage::Tape::TapeSerialiser &serialiser) { proceed_to_symbol(serialiser, SymbolType::Word); return get_next_byte_contents(serialiser); } /*! Reads the next nine symbols and applies a binary test to each to differentiate between ::One and not-::One. Returns a byte composed of the first eight of those as bits; sets the error flag if any symbol is not ::One and not ::Zero, or if the ninth bit is not equal to the odd parity of the other eight. */ uint8_t Parser::get_next_byte_contents(Storage::Tape::TapeSerialiser &serialiser) { int byte_plus_parity = 0; int c = 9; while(c--) { const SymbolType next_symbol = get_next_symbol(serialiser); if((next_symbol != SymbolType::One) && (next_symbol != SymbolType::Zero)) set_error_flag(); byte_plus_parity = (byte_plus_parity >> 1) | (((next_symbol == SymbolType::One) ? 1 : 0) << 8); } int check = byte_plus_parity; check ^= (check >> 4); check ^= (check >> 2); check ^= (check >> 1); if((check&1) == (byte_plus_parity >> 8)) set_error_flag(); add_parity_byte(uint8_t(byte_plus_parity)); return uint8_t(byte_plus_parity); } /*! Returns the result of two consecutive @c get_next_byte calls, arranged in little-endian format. */ uint16_t Parser::get_next_short(Storage::Tape::TapeSerialiser &serialiser) { uint16_t value = get_next_byte(serialiser); value |= get_next_byte(serialiser) << 8; return value; } float Parser::expected_length(const WaveType type) const { const size_t index = size_t(type); if(index < timing_records_.size()) { return timing_records_[index].total / float(timing_records_[index].count); } return 0.0f; } /*! Per the contract with Analyser::Static::TapeParser; sums time across pulses. If this pulse indicates a high to low transition, inspects the time since the last transition, to produce a long, medium, short or unrecognised wave period. */ void Parser::process_pulse(const Storage::Tape::Pulse &pulse) { // The Complete Commodore Inner Space Anthology, P 97, gives half-cycle lengths of: // short: 182us => 0.000364s cycle // medium: 262us => 0.000524s cycle // long: 342us => 0.000684s cycle // The C16, which polls for tape level around lengthy bad line pauses, instead uses these timings: // short: 240us => 0.000480s cycle // medium: 480us => 0.000960s cycle // long: 960us => 0.001920s cycle const auto classify_as = [&](const WaveType type) { push_wave(type); const size_t index = size_t(type); if(index < timing_records_.size() && timing_records_[index].count < 10) { ++timing_records_[index].count; timing_records_[index].total += wave_period_; } }; const bool is_high = pulse.type == Storage::Tape::Pulse::High; if(!is_high && previous_was_high_) { const bool is_plus4 = target_platform_ == TargetPlatform::Plus4; const float short_ms = is_plus4 ? 240.0f : 182.0f; const float medium_ms = is_plus4 ? 480.0f : 262.0f; const float long_ms = is_plus4 ? 960.0f : 342.0f; static constexpr float to_s = 2.0f / 1'000'000.0f; const float overlong_threshold = long_ms * 2.0f * to_s; const float long_threshold = ((long_ms + medium_ms) * 0.5f) * to_s; const float medium_threshold = ((medium_ms + short_ms) * 0.5f) * to_s; const float short_threshold = (short_ms * 0.5f) * to_s; if(wave_period_ >= overlong_threshold) { classify_as(WaveType::Unrecognised); } else if(wave_period_ >= long_threshold) { classify_as(WaveType::Long); } else if(wave_period_ >= medium_threshold) { classify_as(WaveType::Medium); } else if(wave_period_ >= short_threshold) { classify_as(WaveType::Short); } else { classify_as(WaveType::Unrecognised); } wave_period_ = 0.0f; } wave_period_ += pulse.length.as(); previous_was_high_ = is_high; } /*! Per the contract with Analyser::Static::TapeParser; produces any of a word marker, an end-of-block marker, a zero, a one or a lead-in symbol based on the currently captured waves. */ void Parser::inspect_waves(const std::vector &waves) { if(waves.size() < 2) return; if(waves[0] == WaveType::Long && waves[1] == WaveType::Medium) { push_symbol(SymbolType::Word, 2); return; } if(waves[0] == WaveType::Long && waves[1] == WaveType::Short) { push_symbol(SymbolType::EndOfBlock, 2); return; } if(waves[0] == WaveType::Short && waves[1] == WaveType::Medium) { push_symbol(SymbolType::Zero, 2); return; } if(waves[0] == WaveType::Medium && waves[1] == WaveType::Short) { push_symbol(SymbolType::One, 2); return; } if(waves[0] == WaveType::Short) { push_symbol(SymbolType::LeadIn, 1); return; } // Otherwise, eject at least one wave as all options are exhausted. remove_waves(1); }