diff --git a/Analyser/Static/AmstradCPC/StaticAnalyser.cpp b/Analyser/Static/AmstradCPC/StaticAnalyser.cpp index ea1834a34..915275afe 100644 --- a/Analyser/Static/AmstradCPC/StaticAnalyser.cpp +++ b/Analyser/Static/AmstradCPC/StaticAnalyser.cpp @@ -15,8 +15,11 @@ #include "../../../Storage/Disk/Parsers/CPM.hpp" #include "../../../Storage/Disk/Encodings/MFM/Parser.hpp" +#include "../../../Storage/Tape/Parsers/Spectrum.hpp" -static bool strcmp_insensitive(const char *a, const char *b) { +namespace { + +bool strcmp_insensitive(const char *a, const char *b) { if(std::strlen(a) != std::strlen(b)) return false; while(*a) { if(std::tolower(*a) != std::tolower(*b)) return false; @@ -26,20 +29,20 @@ static bool strcmp_insensitive(const char *a, const char *b) { return true; } -static bool is_implied_extension(const std::string &extension) { +bool is_implied_extension(const std::string &extension) { return extension == " " || strcmp_insensitive(extension.c_str(), "BAS") || strcmp_insensitive(extension.c_str(), "BIN"); } -static void right_trim(std::string &string) { +void right_trim(std::string &string) { string.erase(std::find_if(string.rbegin(), string.rend(), [](int ch) { return !std::isspace(ch); }).base(), string.end()); } -static std::string RunCommandFor(const Storage::Disk::CPM::File &file) { +std::string RunCommandFor(const Storage::Disk::CPM::File &file) { // Trim spaces from the name. std::string name = file.name; right_trim(name); @@ -58,7 +61,7 @@ static std::string RunCommandFor(const Storage::Disk::CPM::File &file) { return command + "\n"; } -static void InspectCatalogue( +void InspectCatalogue( const Storage::Disk::CPM::Catalogue &catalogue, const std::unique_ptr &target) { @@ -155,7 +158,7 @@ static void InspectCatalogue( target->loading_command = "cat\n"; } -static bool CheckBootSector(const std::shared_ptr &disk, const std::unique_ptr &target) { +bool CheckBootSector(const std::shared_ptr &disk, const std::unique_ptr &target) { Storage::Encodings::MFM::Parser parser(true, disk); Storage::Encodings::MFM::Sector *boot_sector = parser.get_sector(0, 0, 0x41); if(boot_sector != nullptr && !boot_sector->samples.empty() && boot_sector->samples[0].size() == 512) { @@ -179,6 +182,28 @@ static bool CheckBootSector(const std::shared_ptr &disk, co return false; } +bool IsAmstradTape(const std::shared_ptr &tape) { + // Limited sophistication here; look for a CPC-style file header, that is + // any Spectrum-esque block with a synchronisation character of 0x2c. + // + // More could be done here: parse the header, look for 0x16 data records. + using Parser = Storage::Tape::ZXSpectrum::Parser; + Parser parser(Parser::MachineType::AmstradCPC); + + while(true) { + const auto block = parser.find_block(tape); + if(!block) break; + + if(block->type == 0x2c) { + return true; + } + } + + return false; +} + +} // namespace + Analyser::Static::TargetList Analyser::Static::AmstradCPC::GetTargets(const Media &media, const std::string &, TargetPlatform::IntType) { TargetList destination; auto target = std::make_unique(); @@ -187,13 +212,19 @@ Analyser::Static::TargetList Analyser::Static::AmstradCPC::GetTargets(const Medi target->model = Target::Model::CPC6128; if(!media.tapes.empty()) { - // TODO: which of these are actually potentially CPC tapes? - target->media.tapes = media.tapes; + bool has_cpc_tape = false; + for(auto &tape: media.tapes) { + has_cpc_tape |= IsAmstradTape(tape); + } - // Ugliness flows here: assume the CPC isn't smart enough to pause between pressing - // enter and responding to the follow-on prompt to press a key, so just type for - // a while. Yuck! - target->loading_command = "|tape\nrun\"\n1234567890"; + if(has_cpc_tape) { + target->media.tapes = media.tapes; + + // Ugliness flows here: assume the CPC isn't smart enough to pause between pressing + // enter and responding to the follow-on prompt to press a key, so just type for + // a while. Yuck! + target->loading_command = "|tape\nrun\"\n1234567890"; + } } if(!media.disks.empty()) { diff --git a/Numeric/CRC.hpp b/Numeric/CRC.hpp index 9e10c9a41..5861c56e1 100644 --- a/Numeric/CRC.hpp +++ b/Numeric/CRC.hpp @@ -14,6 +14,18 @@ namespace CRC { +constexpr uint8_t reverse_byte(uint8_t byte) { + return + ((byte & 0x80) ? 0x01 : 0x00) | + ((byte & 0x40) ? 0x02 : 0x00) | + ((byte & 0x20) ? 0x04 : 0x00) | + ((byte & 0x10) ? 0x08 : 0x00) | + ((byte & 0x08) ? 0x10 : 0x00) | + ((byte & 0x04) ? 0x20 : 0x00) | + ((byte & 0x02) ? 0x40 : 0x00) | + ((byte & 0x01) ? 0x80 : 0x00); +} + /*! Provides a class capable of generating a CRC from source data. */ template class Generator { public: @@ -90,18 +102,6 @@ template // -// Source used for the logic below was primarily https://sinclair.wiki.zxnet.co.uk/wiki/Spectrum_tape_interface +// Sources used for the logic below: +// +// https://sinclair.wiki.zxnet.co.uk/wiki/Spectrum_tape_interface +// http://www.cpctech.cpc-live.com/docs/manual/s968se08.pdf +// https://www.alessandrogrussu.it/tapir/tzxform120.html // using namespace Storage::Tape::ZXSpectrum; +Parser::Parser(MachineType machine_type) : + machine_type_(machine_type) {} + void Parser::process_pulse(const Storage::Tape::Tape::Pulse &pulse) { if(pulse.type == Storage::Tape::Tape::Pulse::Type::Zero) { push_wave(WaveType::Gap); @@ -25,44 +34,102 @@ void Parser::process_pulse(const Storage::Tape::Tape::Pulse &pulse) { // Only pulse duration matters; the ZX Spectrum et al do not rely on polarity. const float t_states = pulse.length.get() * 3'500'000.0f; - // Too long => gap. - if(t_states > 2400.0f) { + switch(speed_phase_) { + case SpeedDetectionPhase::WaitingForGap: + // A gap is: any 'pulse' of at least 3000 t-states. + if(t_states >= 3000.0f) { + speed_phase_ = SpeedDetectionPhase::WaitingForPilot; + } + return; + + case SpeedDetectionPhase::WaitingForPilot: + // Pilot tone might be: any pulse of less than 3000 t-states. + if(t_states >= 3000.0f) return; + speed_phase_ = SpeedDetectionPhase::CalibratingPilot; + calibration_pulse_pointer_ = 0; + [[fallthrough]]; + + case SpeedDetectionPhase::CalibratingPilot: { + // Pilot calibration: await at least 8 consecutive pulses of similar length. + calibration_pulses_[calibration_pulse_pointer_] = t_states; + ++calibration_pulse_pointer_; + + // Decide whether it looks like this isn't actually pilot tone. + float mean = 0.0f; + for(size_t c = 0; c < calibration_pulse_pointer_; c++) { + mean += calibration_pulses_[c]; + } + mean /= float(calibration_pulse_pointer_); + for(size_t c = 0; c < calibration_pulse_pointer_; c++) { + if(calibration_pulses_[c] < mean * 0.9f || calibration_pulses_[c] > mean * 1.1f) { + speed_phase_ = SpeedDetectionPhase::WaitingForGap; + return; + } + } + + // Advance only if 8 are present. + if(calibration_pulse_pointer_ == calibration_pulses_.size()) { + speed_phase_ = SpeedDetectionPhase::Done; + + // Note at least one full cycle of pilot tone. + push_wave(WaveType::Pilot); + push_wave(WaveType::Pilot); + + // Configure proper parameters for the autodetection machines. + switch(machine_type_) { + default: break; + + case MachineType::AmstradCPC: + // CPC: pilot tone is length of bit 1; bit 0 is half that. + // So no more detecting formal pilot waves. + is_one_ = mean * 0.75f; + too_long_ = mean * 1.0f / 0.75f; + too_short_ = is_one_ * 0.5f; + is_pilot_ = too_long_; + break; + + case MachineType::Enterprise: + // There's a third validation check here: is this one of the two + // permitted recording speeds? + if(!( + (mean >= 742.0f*0.9f && mean <= 742.0f*1.0f/0.9f) || + (mean >= 1750.0f*0.9f && mean <= 1750.0f*1.0f/0.9f) + )) { + speed_phase_ = SpeedDetectionPhase::WaitingForGap; + return; + } + + // TODO: not yet supported. As below, needs to deal with sync != zero. + assert(false); + break; + + case MachineType::SAMCoupe: { + // TODO: not yet supported. Specifically because I don't think my sync = zero + // assumption even vaguely works here? + assert(false); + } break; + } + } + } return; + + default: + break; + } + + // Too long or too short => gap. + if(t_states >= too_long_ || t_states <= too_short_) { push_wave(WaveType::Gap); return; } - // 1940–2400 t-states => pilot. - if(t_states > 1940.0f) { + // Potentially announce pilot. + if(t_states >= is_pilot_) { push_wave(WaveType::Pilot); return; } - // 1282–1940 t-states => one. - if(t_states > 1282.0f) { - push_wave(WaveType::One); - return; - } - - // 895–1282 => zero. - if(t_states > 795.0f) { - push_wave(WaveType::Zero); - return; - } - - // 701–895 => sync 2. - if(t_states > 701.0f) { - push_wave(WaveType::Sync2); - return; - } - - // Anything remaining above 600 => sync 1. - if(t_states > 600.0f) { - push_wave(WaveType::Sync1); - return; - } - - // Whatever this was, it's too short. Call it a gap. - push_wave(WaveType::Gap); + // Otherwise it's either a one or a zero. + push_wave(t_states > is_one_ ? WaveType::One : WaveType::Zero); } void Parser::inspect_waves(const std::vector &waves) { @@ -71,21 +138,6 @@ void Parser::inspect_waves(const std::vector Parser::find_header(const std::shared_ptr &tape) { +std::optional Parser::find_block(const std::shared_ptr &tape) { + // Decide whether to kick off a speed detection phase. + if(should_detect_speed()) { + speed_phase_ = SpeedDetectionPhase::WaitingForGap; + } + // Find pilot tone. proceed_to_symbol(tape, SymbolType::Pilot); if(is_at_end(tape)) return std::nullopt; - // Find sync. - proceed_to_symbol(tape, SymbolType::Sync); + // Find sync bit. + proceed_to_symbol(tape, SymbolType::Zero); if(is_at_end(tape)) return std::nullopt; - // Read market byte. + // Read marker byte. const auto type = get_byte(tape); if(!type) return std::nullopt; - // TODO: possibly 0x00 is just the Spectrum's preferred identifier; a CPC reference - // suggests it might be 0x16 for data, 0x2c for a header on that platform. - // - // Which would be fantastic for automatically recognising tapes. But we'll see. - if(*type != 0x00) return std::nullopt; - reset_checksum(); - - // Read header contents. - uint8_t header_bytes[17]; - for(size_t c = 0; c < sizeof(header_bytes); c++) { - const auto next_byte = get_byte(tape); - if(!next_byte) return std::nullopt; - header_bytes[c] = *next_byte; - } - - // Check checksum. - const auto post_checksum = get_byte(tape); - if(!post_checksum || *post_checksum) return std::nullopt; - - // Unpack and return. - Header header; - header.type = header_bytes[0]; - memcpy(&header.name, &header_bytes[1], 10); - header.data_length = uint16_t(header_bytes[11] | (header_bytes[12] << 8)); - header.parameters[0] = uint16_t(header_bytes[13] | (header_bytes[14] << 8)); - header.parameters[1] = uint16_t(header_bytes[15] | (header_bytes[16] << 8)); - return header; + // That succeeded. + Block block = { + .type = *type + }; + return block; } -void Parser::reset_checksum() { - checksum_ = 0; +std::vector Parser::get_block_body(const std::shared_ptr &tape) { + std::vector result; + + while(true) { + const auto next_byte = get_byte(tape); + if(!next_byte) break; + result.push_back(*next_byte); + } + + return result; +} + +void Parser::seed_checksum(uint8_t value) { + checksum_ = value; } std::optional Parser::get_byte(const std::shared_ptr &tape) { @@ -152,6 +199,11 @@ std::optional Parser::get_byte(const std::shared_ptr #include +#include namespace Storage { namespace Tape { @@ -22,99 +24,99 @@ enum class WaveType { // ZX Spectrum's 3.5Mhz processor. Pilot, // Nominally 2168 t-states. - Sync1, // 667 t-states. - Sync2, // 735 t-states. Zero, // 855 t-states. One, // 1710 t-states. Gap, }; +// Formally, there are two other types of wave: +// +// Sync1, // 667 t-states. +// Sync2, // 735 t-states. +// +// Non-Spectrum machines often just output a plain zero symbol instead of +// a two-step sync; this parser treats anything close enough to a zero +// as a sync. + enum class SymbolType { - Pilot, - Sync, Zero, One, + Pilot, Gap, }; -struct Header { +/// A block is anything that follows a period of pilot tone; on a Spectrum that might be a +/// file header or the file contents; on a CPC it might be a file header or a single chunk providing +/// partial file contents. The Enterprise seems broadly to follow the Spectrum but the internal +/// byte structure differs. +struct Block { uint8_t type = 0; - char name[11]{}; // 10 bytes on tape; always given a NULL terminator in this code. - uint16_t data_length = 0; - uint16_t parameters[2] = {0, 0}; - - enum class Type { - Program = 0, - NumberArray = 1, - CharacterArray = 2, - Code = 3, - Unknown - }; - Type decoded_type() { - if(type > 3) return Type::Unknown; - return Type(type); - } - - struct BasicParameters { - std::optional autostart_line_number; - uint16_t start_of_variable_area; - }; - BasicParameters basic_parameters() { - const BasicParameters params = { - .autostart_line_number = parameters[0] < 32768 ? std::make_optional(parameters[0]) : std::nullopt, - .start_of_variable_area = parameters[1] - }; - return params; - } - - struct CodeParameters { - uint16_t start_address; - }; - CodeParameters code_parameters() { - const CodeParameters params = { - .start_address = parameters[0] - }; - return params; - } - - struct DataParameters { - char name; - enum class Type { - Numeric, - String - } type; - }; - DataParameters data_parameters() { - #if TARGET_RT_BIG_ENDIAN - const uint8_t data_name = uint8_t(parameters[0]); - #else - const uint8_t data_name = uint8_t(parameters[0] >> 8); - #endif - - using Type = DataParameters::Type; - const DataParameters params = { - .name = char((data_name & 0x1f) + 'a'), - .type = (data_name & 0x40) ? Type::String : Type::Numeric - }; - return params; - } }; class Parser: public Storage::Tape::PulseClassificationParser { public: - /*! - Finds the next header from the tape, if any. - */ - std::optional
find_header(const std::shared_ptr &tape); + enum class MachineType { + ZXSpectrum, + Enterprise, + SAMCoupe, + AmstradCPC + }; + Parser(MachineType); - void reset_checksum(); + /*! + Finds the next block from the tape, if any. + + Following this call the tape will be positioned immediately after the byte that indicated the block type — + in Spectrum-world this seems to be called the flag byte. This call can therefore be followed up with one + of the get_ methods. + */ + std::optional find_block(const std::shared_ptr &tape); + + /*! + Reads the contents of the rest of this block, until the next gap. + */ + std::vector get_block_body(const std::shared_ptr &tape); + + /*! + Reads a single byte from the tape, if there is one left, updating the internal checksum. + + The checksum is computed as an exclusive OR of all bytes read. + */ std::optional get_byte(const std::shared_ptr &tape); + /*! + Seeds the internal checksum. + */ + void seed_checksum(uint8_t value = 0x00); + private: + const MachineType machine_type_; + constexpr bool should_flip_bytes() { + return machine_type_ == MachineType::Enterprise; + } + constexpr bool should_detect_speed() { + return machine_type_ != MachineType::ZXSpectrum; + } + void process_pulse(const Storage::Tape::Tape::Pulse &pulse) override; void inspect_waves(const std::vector &waves) override; uint8_t checksum_ = 0; + + enum class SpeedDetectionPhase { + WaitingForGap, + WaitingForPilot, + CalibratingPilot, + Done + } speed_phase_ = SpeedDetectionPhase::Done; + + float too_long_ = 2600.0f; + float too_short_ = 600.0f; + float is_pilot_ = 1939.0f; + float is_one_ = 1282.0f; + + std::array calibration_pulses_; + size_t calibration_pulse_pointer_ = 0; }; }