1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-08-07 08:26:28 +00:00
Files
CLK/Storage/Tape/Parsers/Commodore.cpp
2025-01-21 22:37:10 -05:00

312 lines
10 KiB
C++

//
// Commodore.cpp
// Clock Signal
//
// Created by Thomas Harte on 06/11/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#include "Commodore.hpp"
#include <cstring>
#include "../../Data/Commodore.hpp"
using namespace Storage::Tape::Commodore;
Parser::Parser(TargetPlatform::Type target_platform) :
Storage::Tape::PulseClassificationParser<WaveType, SymbolType>(),
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<Header> Parser::get_next_header(Storage::Tape::TapeSerialiser &serialiser) {
return duplicate_match<Header>(
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<Data> Parser::get_next_data(Storage::Tape::TapeSerialiser &serialiser) {
return duplicate_match<Data>(
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<class ObjectType>
std::unique_ptr<ObjectType> Parser::duplicate_match(
std::unique_ptr<ObjectType> first_copy,
std::unique_ptr<ObjectType> 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;
// otherwise plan to return either one with a correct check digit, doing a comparison with the other
std::unique_ptr<ObjectType> *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 = !(memcmp(&first_copy->data[0], &second_copy->data[0], first_copy->data.size()));
return std::move(*copy_to_return);
}
std::unique_ptr<Header> Parser::get_next_header_body(Storage::Tape::TapeSerialiser &serialiser, bool is_original) {
auto header = std::make_unique<Header>();
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 *target, [[maybe_unused]] uint16_t length) const {
target[0] = type_descriptor();
// TODO: validate length.
std::memcpy(&target[1], data.data(), 191);
}
std::unique_ptr<Data> Parser::get_next_data_body(Storage::Tape::TapeSerialiser &serialiser, bool is_original) {
auto data = std::make_unique<Data>();
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 reead the parity byte to the end of the data; if it matched the calculated parity it'll now be zero
data->parity_was_valid = !get_parity_byte();
data->duplicate_matched = false;
// remove the captured parity
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, bool is_original) {
uint8_t landing_zone[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
while(!serialiser.is_at_end()) {
memmove(landing_zone, &landing_zone[1], sizeof(uint8_t) * 8);
landing_zone[8] = get_next_byte(serialiser);
bool is_landing_zone = true;
for(int 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() { return parity_byte_; }
void Parser::add_parity_byte(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;
}
/*!
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 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;
constexpr float to_s = 2.0f / 1'000'000.0f;
const float overlong_threshold = (long_ms + long_ms - medium_ms) * 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) push_wave(WaveType::Unrecognised);
else if(wave_period_ >= long_threshold) push_wave(WaveType::Long);
else if(wave_period_ >= medium_threshold) push_wave(WaveType::Medium);
else if(wave_period_ >= short_threshold) push_wave(WaveType::Short);
else push_wave(WaveType::Unrecognised);
wave_period_ = 0.0f;
}
wave_period_ += pulse.length.get<float>();
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<WaveType> &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);
}