From d93d380c888c16735a6a79ffd9ce9a1869dc58da Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 7 Mar 2021 15:51:25 -0500 Subject: [PATCH 01/18] Adds bit-level Spectrum-style tape parsing. More to do, obviously. --- .../Clock Signal.xcodeproj/project.pbxproj | 8 ++ Storage/Tape/Parsers/Spectrum.cpp | 98 +++++++++++++++++++ Storage/Tape/Parsers/Spectrum.hpp | 48 +++++++++ 3 files changed, 154 insertions(+) create mode 100644 Storage/Tape/Parsers/Spectrum.cpp create mode 100644 Storage/Tape/Parsers/Spectrum.hpp diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 03d6f0e1b..c671eaf43 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -243,6 +243,8 @@ 4B59199C1DAC6C46005BB85C /* OricTAP.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B59199A1DAC6C46005BB85C /* OricTAP.cpp */; }; 4B595FAD2086DFBA0083CAA8 /* AudioToggle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B595FAC2086DFBA0083CAA8 /* AudioToggle.cpp */; }; 4B595FAE2086DFBA0083CAA8 /* AudioToggle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B595FAC2086DFBA0083CAA8 /* AudioToggle.cpp */; }; + 4B5D5C9725F56FC7001B4623 /* Spectrum.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5D5C9525F56FC7001B4623 /* Spectrum.cpp */; }; + 4B5D5C9825F56FC7001B4623 /* Spectrum.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5D5C9525F56FC7001B4623 /* Spectrum.cpp */; }; 4B5FADBA1DE3151600AEC565 /* FileHolder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5FADB81DE3151600AEC565 /* FileHolder.cpp */; }; 4B5FADC01DE3BF2B00AEC565 /* Microdisc.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5FADBE1DE3BF2B00AEC565 /* Microdisc.cpp */; }; 4B622AE5222E0AD5008B59F2 /* DisplayMetrics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B622AE3222E0AD5008B59F2 /* DisplayMetrics.cpp */; }; @@ -1252,6 +1254,8 @@ 4B59199B1DAC6C46005BB85C /* OricTAP.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = OricTAP.hpp; sourceTree = ""; }; 4B595FAB2086DFBA0083CAA8 /* AudioToggle.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = AudioToggle.hpp; sourceTree = ""; }; 4B595FAC2086DFBA0083CAA8 /* AudioToggle.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AudioToggle.cpp; sourceTree = ""; }; + 4B5D5C9525F56FC7001B4623 /* Spectrum.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Spectrum.cpp; path = Parsers/Spectrum.cpp; sourceTree = ""; }; + 4B5D5C9625F56FC7001B4623 /* Spectrum.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Spectrum.hpp; path = Parsers/Spectrum.hpp; sourceTree = ""; }; 4B5FADB81DE3151600AEC565 /* FileHolder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = FileHolder.cpp; sourceTree = ""; }; 4B5FADB91DE3151600AEC565 /* FileHolder.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = FileHolder.hpp; sourceTree = ""; }; 4B5FADBE1DE3BF2B00AEC565 /* Microdisc.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Microdisc.cpp; path = Oric/Microdisc.cpp; sourceTree = ""; }; @@ -3021,11 +3025,13 @@ 4B8805F21DCFD22A003085B1 /* Commodore.cpp */, 4B0E61051FF34737002A9DBD /* MSX.cpp */, 4B8805F91DCFF807003085B1 /* Oric.cpp */, + 4B5D5C9525F56FC7001B4623 /* Spectrum.cpp */, 4BBFBB6A1EE8401E00C01E7A /* ZX8081.cpp */, 4B8805EF1DCFC99C003085B1 /* Acorn.hpp */, 4B8805F31DCFD22A003085B1 /* Commodore.hpp */, 4B0E61061FF34737002A9DBD /* MSX.hpp */, 4B8805FA1DCFF807003085B1 /* Oric.hpp */, + 4B5D5C9625F56FC7001B4623 /* Spectrum.hpp */, 4B4518A71F76004200926311 /* TapeParser.hpp */, 4BBFBB6B1EE8401E00C01E7A /* ZX8081.hpp */, ); @@ -5050,6 +5056,7 @@ 4B2E86BF25D74F160024F1E9 /* Mouse.cpp in Sources */, 4B6ED2F1208E2F8A0047B343 /* WOZ.cpp in Sources */, 4B055AD81FAE9B180060FFFF /* Video.cpp in Sources */, + 4B5D5C9825F56FC7001B4623 /* Spectrum.cpp in Sources */, 4B2E86D025D8D8C70024F1E9 /* Keyboard.cpp in Sources */, 4B89452F201967B4007DE474 /* StaticAnalyser.cpp in Sources */, 4B894531201967B4007DE474 /* StaticAnalyser.cpp in Sources */, @@ -5256,6 +5263,7 @@ 4B894530201967B4007DE474 /* StaticAnalyser.cpp in Sources */, 4B4518A31F75FD1C00926311 /* HFE.cpp in Sources */, 4B1B88BB202E2EC100B67DFF /* MultiKeyboardMachine.cpp in Sources */, + 4B5D5C9725F56FC7001B4623 /* Spectrum.cpp in Sources */, 4B4518A11F75FD1C00926311 /* D64.cpp in Sources */, 4B1558C01F844ECD006E9A97 /* BitReverse.cpp in Sources */, 4BCE0052227CE8CA000CA200 /* DiskIICard.cpp in Sources */, diff --git a/Storage/Tape/Parsers/Spectrum.cpp b/Storage/Tape/Parsers/Spectrum.cpp new file mode 100644 index 000000000..6b1ff4362 --- /dev/null +++ b/Storage/Tape/Parsers/Spectrum.cpp @@ -0,0 +1,98 @@ +// +// Spectrum.cpp +// Clock Signal +// +// Created by Thomas Harte on 07/03/2021. +// Copyright © 2021 Thomas Harte. All rights reserved. +// + +#include "Spectrum.hpp" + +// +// Source used for the logic below was primarily https://sinclair.wiki.zxnet.co.uk/wiki/Spectrum_tape_interface +// + +using namespace Storage::Tape::ZXSpectrum; + +void Parser::process_pulse(const Storage::Tape::Tape::Pulse &pulse) { + if(pulse.type == Storage::Tape::Tape::Pulse::Type::Zero) { + push_wave(WaveType::Gap); + return; + } + + // 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) { + push_wave(WaveType::Gap); + return; + } + + // 1940–2400 t-states => pilot. + if(t_states > 1940.0f) { + 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); +} + +void Parser::inspect_waves(const std::vector &waves) { + switch(waves[0]) { + // Gap and Pilot map directly. + case WaveType::Gap: push_symbol(SymbolType::Gap, 1); break; + case WaveType::Pilot: push_symbol(SymbolType::Pilot, 1); break; + + // Encountering a sync 2 on its own is unexpected. + case WaveType::Sync2: + push_symbol(SymbolType::Gap, 1); + break; + + // A sync 1 should be followed by a sync 2 in order to make a sync. + case WaveType::Sync1: + if(waves.size() < 2) return; + if(waves[1] == WaveType::Sync2) { + push_symbol(SymbolType::Sync, 2); + } else { + push_symbol(SymbolType::Gap, 1); + } + break; + + // Both one and zero waves should come in pairs. + case WaveType::One: + case WaveType::Zero: + if(waves.size() < 2) return; + if(waves[1] == waves[0]) { + push_symbol(waves[0] == WaveType::One ? SymbolType::One : SymbolType::Zero, 2); + } else { + push_symbol(SymbolType::Gap, 1); + } + break; + } +} diff --git a/Storage/Tape/Parsers/Spectrum.hpp b/Storage/Tape/Parsers/Spectrum.hpp new file mode 100644 index 000000000..817a064d0 --- /dev/null +++ b/Storage/Tape/Parsers/Spectrum.hpp @@ -0,0 +1,48 @@ +// +// Spectrum.hpp +// Clock Signal +// +// Created by Thomas Harte on 07/03/2021. +// Copyright © 2021 Thomas Harte. All rights reserved. +// + +#ifndef Storage_Tape_Parsers_Spectrum_hpp +#define Storage_Tape_Parsers_Spectrum_hpp + +#include "TapeParser.hpp" + +namespace Storage { +namespace Tape { +namespace ZXSpectrum { + +enum class WaveType { + // All references to 't-states' below are cycles relative to the + // 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, +}; + +enum class SymbolType { + Pilot, + Sync, + Zero, + One, + Gap, +}; + +class Parser: public Storage::Tape::PulseClassificationParser { + private: + void process_pulse(const Storage::Tape::Tape::Pulse &pulse) override; + void inspect_waves(const std::vector &waves) override; +}; + +} +} +} + +#endif /* Spectrum_hpp */ From 40516c9cece8a037e09047a8cd548709797162e8 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 7 Mar 2021 15:56:58 -0500 Subject: [PATCH 02/18] Minor style improvements: some local `const`s, and `override`s. --- Storage/Tape/Parsers/Acorn.cpp | 7 ++++--- Storage/Tape/Parsers/Acorn.hpp | 6 +++--- Storage/Tape/Parsers/Commodore.cpp | 13 ++++++------- Storage/Tape/Parsers/Commodore.hpp | 4 ++-- Storage/Tape/Parsers/Oric.cpp | 2 +- Storage/Tape/Parsers/Oric.hpp | 4 ++-- Storage/Tape/Parsers/ZX8081.cpp | 6 +++--- Storage/Tape/Parsers/ZX8081.hpp | 7 +++---- 8 files changed, 24 insertions(+), 25 deletions(-) diff --git a/Storage/Tape/Parsers/Acorn.cpp b/Storage/Tape/Parsers/Acorn.cpp index 4c8895ec2..65c976adb 100644 --- a/Storage/Tape/Parsers/Acorn.cpp +++ b/Storage/Tape/Parsers/Acorn.cpp @@ -24,12 +24,13 @@ int Parser::get_next_bit(const std::shared_ptr &tape) { } int Parser::get_next_byte(const std::shared_ptr &tape) { - int value = 0; - int c = 8; if(get_next_bit(tape)) { set_error_flag(); return -1; } + + int value = 0; + int c = 8; while(c--) { value = (value >> 1) | (get_next_bit(tape) << 7); } @@ -74,7 +75,7 @@ Shifter::Shifter() : void Shifter::process_pulse(const Storage::Tape::Tape::Pulse &pulse) { pll_.run_for(Cycles(int(float(PLLClockRate) * pulse.length.get()))); - bool is_high = pulse.type == Storage::Tape::Tape::Pulse::High; + const bool is_high = pulse.type == Storage::Tape::Tape::Pulse::High; if(is_high != was_high_) { pll_.add_pulse(); } diff --git a/Storage/Tape/Parsers/Acorn.hpp b/Storage/Tape/Parsers/Acorn.hpp index 5d4673701..394f19bac 100644 --- a/Storage/Tape/Parsers/Acorn.hpp +++ b/Storage/Tape/Parsers/Acorn.hpp @@ -57,10 +57,10 @@ class Parser: public Storage::Tape::Parser, public Shifter::Delegate void reset_crc(); uint16_t get_crc(); - void acorn_shifter_output_bit(int value); - void process_pulse(const Storage::Tape::Tape::Pulse &pulse); - private: + void acorn_shifter_output_bit(int value) override; + void process_pulse(const Storage::Tape::Tape::Pulse &pulse) override; + bool did_update_shifter(int new_value, int length); CRC::Generator crc_; Shifter shifter_; diff --git a/Storage/Tape/Parsers/Commodore.cpp b/Storage/Tape/Parsers/Commodore.cpp index a8a15d3bd..7e33c1596 100644 --- a/Storage/Tape/Parsers/Commodore.cpp +++ b/Storage/Tape/Parsers/Commodore.cpp @@ -76,7 +76,7 @@ std::unique_ptr
Parser::get_next_header_body(const std::shared_ptrtype = Header::Unknown; break; case 0x01: header->type = Header::RelocatableProgram; break; @@ -92,7 +92,7 @@ std::unique_ptr
Parser::get_next_header_body(const std::shared_ptrdata.push_back(get_next_byte(tape)); } - uint8_t parity_byte = get_parity_byte(); + const uint8_t parity_byte = get_parity_byte(); header->parity_was_valid = get_next_byte(tape) == parity_byte; // parse if this is not pure data @@ -110,7 +110,7 @@ std::unique_ptr
Parser::get_next_header_body(const std::shared_ptr */ void Parser::proceed_to_symbol(const std::shared_ptr &tape, SymbolType required_symbol) { while(!tape->is_at_end()) { - SymbolType symbol = get_next_symbol(tape); + const SymbolType symbol = get_next_symbol(tape); if(symbol == required_symbol) return; } } @@ -187,7 +186,7 @@ void Parser::proceed_to_symbol(const std::shared_ptr &tape, Swallows the next byte; sets the error flag if it is not equal to @c value. */ void Parser::expect_byte(const std::shared_ptr &tape, uint8_t value) { - uint8_t next_byte = get_next_byte(tape); + const uint8_t next_byte = get_next_byte(tape); if(next_byte != value) set_error_flag(); } @@ -247,7 +246,7 @@ void Parser::process_pulse(const Storage::Tape::Tape::Pulse &pulse) { // short: 182us => 0.000364s cycle // medium: 262us => 0.000524s cycle // long: 342us => 0.000684s cycle - bool is_high = pulse.type == Storage::Tape::Tape::Pulse::High; + const bool is_high = pulse.type == Storage::Tape::Tape::Pulse::High; if(!is_high && previous_was_high_) { if(wave_period_ >= 0.000764) push_wave(WaveType::Unrecognised); else if(wave_period_ >= 0.000604) push_wave(WaveType::Long); diff --git a/Storage/Tape/Parsers/Commodore.hpp b/Storage/Tape/Parsers/Commodore.hpp index 0a64728f4..b7cc8aecb 100644 --- a/Storage/Tape/Parsers/Commodore.hpp +++ b/Storage/Tape/Parsers/Commodore.hpp @@ -126,7 +126,7 @@ class Parser: public Storage::Tape::PulseClassificationParser &waves); + void inspect_waves(const std::vector &waves) override; }; } diff --git a/Storage/Tape/Parsers/Oric.cpp b/Storage/Tape/Parsers/Oric.cpp index ffb98cab2..cc3019bc4 100644 --- a/Storage/Tape/Parsers/Oric.cpp +++ b/Storage/Tape/Parsers/Oric.cpp @@ -45,7 +45,7 @@ void Parser::process_pulse(const Storage::Tape::Tape::Pulse &pulse) { constexpr float maximum_medium_length = 0.000728f; constexpr float maximum_long_length = 0.001456f; - bool wave_is_high = pulse.type == Storage::Tape::Tape::Pulse::High; + const bool wave_is_high = pulse.type == Storage::Tape::Tape::Pulse::High; if(!wave_was_high_ && wave_is_high != wave_was_high_) { if(cycle_length_ < maximum_short_length) push_wave(WaveType::Short); else if(cycle_length_ < maximum_medium_length) push_wave(WaveType::Medium); diff --git a/Storage/Tape/Parsers/Oric.hpp b/Storage/Tape/Parsers/Oric.hpp index 0b46541e9..4a5f2d403 100644 --- a/Storage/Tape/Parsers/Oric.hpp +++ b/Storage/Tape/Parsers/Oric.hpp @@ -32,8 +32,8 @@ class Parser: public Storage::Tape::PulseClassificationParser &tape); private: - void process_pulse(const Storage::Tape::Tape::Pulse &pulse); - void inspect_waves(const std::vector &waves); + void process_pulse(const Storage::Tape::Tape::Pulse &pulse) override; + void inspect_waves(const std::vector &waves) override; enum DetectionMode { FastData, diff --git a/Storage/Tape/Parsers/ZX8081.cpp b/Storage/Tape/Parsers/ZX8081.cpp index 1e11502b5..487c6a398 100644 --- a/Storage/Tape/Parsers/ZX8081.cpp +++ b/Storage/Tape/Parsers/ZX8081.cpp @@ -15,8 +15,8 @@ Parser::Parser() : pulse_was_high_(false), pulse_time_(0) {} void Parser::process_pulse(const Storage::Tape::Tape::Pulse &pulse) { // If this is anything other than a transition from low to high, just add it to the // count of time. - bool pulse_is_high = pulse.type == Storage::Tape::Tape::Pulse::High; - bool pulse_did_change = pulse_is_high != pulse_was_high_; + const bool pulse_is_high = pulse.type == Storage::Tape::Tape::Pulse::High; + const bool pulse_did_change = pulse_is_high != pulse_was_high_; pulse_was_high_ = pulse_is_high; if(!pulse_did_change || !pulse_is_high) { pulse_time_ += pulse.length; @@ -31,7 +31,7 @@ void Parser::process_pulse(const Storage::Tape::Tape::Pulse &pulse) { void Parser::post_pulse() { constexpr float expected_pulse_length = 300.0f / 1000000.0f; constexpr float expected_gap_length = 1300.0f / 1000000.0f; - auto pulse_time = pulse_time_.get(); + const auto pulse_time = pulse_time_.get(); if(pulse_time > expected_gap_length * 1.25f) { push_wave(WaveType::LongGap); diff --git a/Storage/Tape/Parsers/ZX8081.hpp b/Storage/Tape/Parsers/ZX8081.hpp index bb32f7cdd..a3f3b8de7 100644 --- a/Storage/Tape/Parsers/ZX8081.hpp +++ b/Storage/Tape/Parsers/ZX8081.hpp @@ -50,10 +50,9 @@ class Parser: public Storage::Tape::PulseClassificationParser &waves); + void process_pulse(const Storage::Tape::Tape::Pulse &pulse) override; + void mark_end() override; + void inspect_waves(const std::vector &waves) override; std::shared_ptr> get_next_file_data(const std::shared_ptr &tape); }; From ab5e4ca9c70969720de661c702b4c6955b25173e Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 7 Mar 2021 20:48:51 -0500 Subject: [PATCH 03/18] Factors `proceed_to_symbol` upwards. --- Storage/Tape/Parsers/Commodore.cpp | 15 ++------------- Storage/Tape/Parsers/Commodore.hpp | 6 ------ Storage/Tape/Parsers/TapeParser.hpp | 11 +++++++++++ 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/Storage/Tape/Parsers/Commodore.cpp b/Storage/Tape/Parsers/Commodore.cpp index 7e33c1596..ea3bea801 100644 --- a/Storage/Tape/Parsers/Commodore.cpp +++ b/Storage/Tape/Parsers/Commodore.cpp @@ -136,7 +136,7 @@ std::unique_ptr Parser::get_next_data_body(const std::shared_ptris_at_end()) { - SymbolType start_symbol = get_next_symbol(tape); + const SymbolType start_symbol = get_next_symbol(tape); if(start_symbol != SymbolType::Word) break; data->data.push_back(get_next_byte_contents(tape)); } @@ -171,17 +171,6 @@ void Parser::proceed_to_landing_zone(const std::shared_ptr } } -/*! - Swallows symbols until it reaches the first instance of the required symbol, swallows that - and returns. -*/ -void Parser::proceed_to_symbol(const std::shared_ptr &tape, SymbolType required_symbol) { - while(!tape->is_at_end()) { - const SymbolType symbol = get_next_symbol(tape); - if(symbol == required_symbol) return; - } -} - /*! Swallows the next byte; sets the error flag if it is not equal to @c value. */ @@ -211,7 +200,7 @@ uint8_t Parser::get_next_byte_contents(const std::shared_ptr> 1) | (((next_symbol == SymbolType::One) ? 1 : 0) << 8); } diff --git a/Storage/Tape/Parsers/Commodore.hpp b/Storage/Tape/Parsers/Commodore.hpp index b7cc8aecb..8a5040dc7 100644 --- a/Storage/Tape/Parsers/Commodore.hpp +++ b/Storage/Tape/Parsers/Commodore.hpp @@ -88,12 +88,6 @@ class Parser: public Storage::Tape::PulseClassificationParser &tape, bool is_original); - /*! - Swallows symbols until it reaches the first instance of the required symbol, swallows that - and returns. - */ - void proceed_to_symbol(const std::shared_ptr &tape, SymbolType required_symbol); - /*! Swallows the next byte; sets the error flag if it is not equal to @c value. */ diff --git a/Storage/Tape/Parsers/TapeParser.hpp b/Storage/Tape/Parsers/TapeParser.hpp index 478ee21c6..774a87002 100644 --- a/Storage/Tape/Parsers/TapeParser.hpp +++ b/Storage/Tape/Parsers/TapeParser.hpp @@ -56,6 +56,17 @@ template class Parser { return tape->is_at_end() && !has_next_symbol_; } + /*! + Swallows symbols until it reaches the first instance of the required symbol, swallows that + and returns. + */ + void proceed_to_symbol(const std::shared_ptr &tape, SymbolType required_symbol) { + while(!is_at_end(tape)) { + const SymbolType symbol = get_next_symbol(tape); + if(symbol == required_symbol) return; + } + } + protected: /*! Should be implemented by subclasses. Consumes @c pulse. From e9177bbb2ac1298d5efe0582ef7a29bb8334cb7b Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 7 Mar 2021 20:49:09 -0500 Subject: [PATCH 04/18] Makes an attempt to parse headers. --- Storage/Tape/Parsers/Spectrum.cpp | 52 +++++++++++++++++++++ Storage/Tape/Parsers/Spectrum.hpp | 76 +++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/Storage/Tape/Parsers/Spectrum.cpp b/Storage/Tape/Parsers/Spectrum.cpp index 6b1ff4362..a3221cf3e 100644 --- a/Storage/Tape/Parsers/Spectrum.cpp +++ b/Storage/Tape/Parsers/Spectrum.cpp @@ -96,3 +96,55 @@ void Parser::inspect_waves(const std::vector Parser::find_header(const std::shared_ptr &tape) { + // 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); + if(is_at_end(tape)) return std::nullopt; + + // Read market byte. + const auto type = get_byte(tape); + if(!type) return std::nullopt; + 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; +} + +void Parser::reset_checksum() { + checksum_ = 0; +} + +std::optional Parser::get_byte(const std::shared_ptr &tape) { + uint8_t result = 0; + for(int c = 0; c < 8; c++) { + const SymbolType symbol = get_next_symbol(tape); + if(symbol != SymbolType::One && symbol != SymbolType::Zero) return std::nullopt; + result = uint8_t((result << 1) | (symbol == SymbolType::One)); + } + checksum_ ^= result; + return result; +} diff --git a/Storage/Tape/Parsers/Spectrum.hpp b/Storage/Tape/Parsers/Spectrum.hpp index 817a064d0..aa2c0bb34 100644 --- a/Storage/Tape/Parsers/Spectrum.hpp +++ b/Storage/Tape/Parsers/Spectrum.hpp @@ -11,6 +11,8 @@ #include "TapeParser.hpp" +#include + namespace Storage { namespace Tape { namespace ZXSpectrum { @@ -35,10 +37,84 @@ enum class SymbolType { Gap, }; +struct Header { + 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); + + void reset_checksum(); + std::optional get_byte(const std::shared_ptr &tape); + private: void process_pulse(const Storage::Tape::Tape::Pulse &pulse) override; void inspect_waves(const std::vector &waves) override; + + uint8_t checksum_ = 0; }; } From 5c90744f0c448b02034e4a56c31b11573999d8cf Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 7 Mar 2021 20:49:40 -0500 Subject: [PATCH 05/18] More minor style improvements. --- Storage/Tape/Parsers/Acorn.cpp | 6 +++--- Storage/Tape/Parsers/Acorn.hpp | 2 +- Storage/Tape/Parsers/Oric.hpp | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Storage/Tape/Parsers/Acorn.cpp b/Storage/Tape/Parsers/Acorn.cpp index 65c976adb..d23ade030 100644 --- a/Storage/Tape/Parsers/Acorn.cpp +++ b/Storage/Tape/Parsers/Acorn.cpp @@ -19,7 +19,7 @@ Parser::Parser(): crc_(0x1021) { } int Parser::get_next_bit(const std::shared_ptr &tape) { - SymbolType symbol = get_next_symbol(tape); + const SymbolType symbol = get_next_symbol(tape); return (symbol == SymbolType::One) ? 1 : 0; } @@ -54,8 +54,8 @@ unsigned int Parser::get_next_word(const std::shared_ptr &t return result; } -void Parser::reset_crc() { crc_.reset(); } -uint16_t Parser::get_crc() { return crc_.get_value(); } +void Parser::reset_crc() { crc_.reset(); } +uint16_t Parser::get_crc() const { return crc_.get_value(); } void Parser::acorn_shifter_output_bit(int value) { push_symbol(value ? SymbolType::One : SymbolType::Zero); diff --git a/Storage/Tape/Parsers/Acorn.hpp b/Storage/Tape/Parsers/Acorn.hpp index 394f19bac..8fd762f2e 100644 --- a/Storage/Tape/Parsers/Acorn.hpp +++ b/Storage/Tape/Parsers/Acorn.hpp @@ -55,7 +55,7 @@ class Parser: public Storage::Tape::Parser, public Shifter::Delegate unsigned int get_next_short(const std::shared_ptr &tape); unsigned int get_next_word(const std::shared_ptr &tape); void reset_crc(); - uint16_t get_crc(); + uint16_t get_crc() const; private: void acorn_shifter_output_bit(int value) override; diff --git a/Storage/Tape/Parsers/Oric.hpp b/Storage/Tape/Parsers/Oric.hpp index 4a5f2d403..bfbad1b86 100644 --- a/Storage/Tape/Parsers/Oric.hpp +++ b/Storage/Tape/Parsers/Oric.hpp @@ -45,8 +45,7 @@ class Parser: public Storage::Tape::PulseClassificationParser Date: Sun, 7 Mar 2021 21:20:35 -0500 Subject: [PATCH 06/18] Add header for memcpy. --- Storage/Tape/Parsers/Spectrum.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Storage/Tape/Parsers/Spectrum.cpp b/Storage/Tape/Parsers/Spectrum.cpp index a3221cf3e..138fe75c6 100644 --- a/Storage/Tape/Parsers/Spectrum.cpp +++ b/Storage/Tape/Parsers/Spectrum.cpp @@ -8,6 +8,8 @@ #include "Spectrum.hpp" +#include + // // Source used for the logic below was primarily https://sinclair.wiki.zxnet.co.uk/wiki/Spectrum_tape_interface // From 4eaf3440bd18c73ff787964cb4b474a4ecec0797 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 7 Mar 2021 21:21:58 -0500 Subject: [PATCH 07/18] Add note to self. --- Storage/Tape/Parsers/Spectrum.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Storage/Tape/Parsers/Spectrum.cpp b/Storage/Tape/Parsers/Spectrum.cpp index 138fe75c6..26224d456 100644 --- a/Storage/Tape/Parsers/Spectrum.cpp +++ b/Storage/Tape/Parsers/Spectrum.cpp @@ -111,6 +111,11 @@ std::optional
Parser::find_header(const std::shared_ptr Date: Wed, 10 Mar 2021 22:02:10 -0500 Subject: [PATCH 08/18] Enables detection of CPC-format tape data. It turns out that the Spectrum's timings are its alone; speed autodetection added. --- Analyser/Static/AmstradCPC/StaticAnalyser.cpp | 55 ++++- Numeric/CRC.hpp | 24 +- Storage/Tape/Parsers/Spectrum.cpp | 210 +++++++++++------- Storage/Tape/Parsers/Spectrum.hpp | 140 ++++++------ 4 files changed, 257 insertions(+), 172 deletions(-) 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; }; } From cd215ef52121119c657f089721e0e8a4ee5c8547 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 12 Mar 2021 18:42:17 -0500 Subject: [PATCH 09/18] Stumbles towards supporting fast tape loading. Right now: in a non-optional manner. --- Machines/AmstradCPC/AmstradCPC.cpp | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp index 485bc162c..6595fa47e 100644 --- a/Machines/AmstradCPC/AmstradCPC.cpp +++ b/Machines/AmstradCPC/AmstradCPC.cpp @@ -24,6 +24,7 @@ #include "../MachineTypes.hpp" #include "../../Storage/Tape/Tape.hpp" +#include "../../Storage/Tape/Parsers/Spectrum.hpp" #include "../../ClockReceiver/ForceInline.hpp" #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" @@ -915,6 +916,30 @@ template class ConcreteMachine: uint16_t address = cycle.address ? *cycle.address : 0x0000; switch(cycle.operation) { case CPU::Z80::PartialMachineCycle::ReadOpcode: + if(address == tape_read_byte_address && read_pointers_[0] == roms_[ROMType::OS].data()) { + using Parser = Storage::Tape::ZXSpectrum::Parser; + Parser parser(Parser::MachineType::AmstradCPC); + parser.set_cpc_read_speed(read_pointers_[tape_speed_value_address >> 14][tape_speed_value_address & 16383]); + + const auto byte = parser.get_byte(tape_player_.get_tape()); + auto flags = z80_.get_value_of_register(CPU::Z80::Register::Flags); + + if(byte) { + z80_.set_value_of_register(CPU::Z80::Register::A, *byte); + flags |= CPU::Z80::Flag::Carry; + } else { + // TODO: return tape player to previous state and decline to serve. + z80_.set_value_of_register(CPU::Z80::Register::A, 0); + flags &= ~CPU::Z80::Flag::Carry; + } + z80_.set_value_of_register(CPU::Z80::Register::Flags, flags); + + // RET. + *cycle.value = 0xc9; + break; + } + + [[fallthrough]]; case CPU::Z80::PartialMachineCycle::Read: *cycle.value = read_pointers_[address >> 14][address & 16383]; break; @@ -1203,6 +1228,11 @@ template class ConcreteMachine: InterruptTimer interrupt_timer_; Storage::Tape::BinaryTapePlayer tape_player_; + // By luck these values are the same between the 664 and the 6128; + // therefore the has_fdc template flag is sufficient to locate them. + static constexpr uint16_t tape_read_byte_address = has_fdc ? 0x2b20 : 0x29b0; + static constexpr uint16_t tape_speed_value_address = has_fdc ? 0xb1e7 : 0xbc8f; + HalfCycles clock_offset_; HalfCycles crtc_counter_; HalfCycles half_cycles_since_ay_update_; From 064fe7658cb35a035872c83329487d11f78e1d84 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 12 Mar 2021 18:43:20 -0500 Subject: [PATCH 10/18] Adds necessary interface to inherit a CPC tape-speed byte. --- Storage/Tape/Parsers/Spectrum.cpp | 19 +++++++++++++++---- Storage/Tape/Parsers/Spectrum.hpp | 7 +++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Storage/Tape/Parsers/Spectrum.cpp b/Storage/Tape/Parsers/Spectrum.cpp index bcf18194c..4a884875a 100644 --- a/Storage/Tape/Parsers/Spectrum.cpp +++ b/Storage/Tape/Parsers/Spectrum.cpp @@ -82,10 +82,7 @@ void Parser::process_pulse(const Storage::Tape::Tape::Pulse &pulse) { 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_; + set_cpc_one_zero_boundary(mean * 0.75f); break; case MachineType::Enterprise: @@ -132,6 +129,20 @@ void Parser::process_pulse(const Storage::Tape::Tape::Pulse &pulse) { push_wave(t_states > is_one_ ? WaveType::One : WaveType::Zero); } +void Parser::set_cpc_read_speed(uint8_t speed) { + // This may not be exactly right; I wish there were more science here but + // instead it's empirical based on tape speed versus value stored plus + // a guess as to where the CPC puts the dividing line. + set_cpc_one_zero_boundary(float(speed) * 14.35f); +} + +void Parser::set_cpc_one_zero_boundary(float boundary) { + is_one_ = boundary; + too_long_ = is_one_ * 16.0f / 9.0f; + too_short_ = is_one_ * 0.5f; + is_pilot_ = too_long_; +} + void Parser::inspect_waves(const std::vector &waves) { switch(waves[0]) { // Gap and Pilot map directly. diff --git a/Storage/Tape/Parsers/Spectrum.hpp b/Storage/Tape/Parsers/Spectrum.hpp index e8a2d5b32..2b99fcbfb 100644 --- a/Storage/Tape/Parsers/Spectrum.hpp +++ b/Storage/Tape/Parsers/Spectrum.hpp @@ -63,6 +63,11 @@ class Parser: public Storage::Tape::PulseClassificationParser calibration_pulses_; size_t calibration_pulse_pointer_ = 0; + + void set_cpc_one_zero_boundary(float); }; } From a32a2f36be6369478bc18fac78ff50b6398d9882 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 12 Mar 2021 19:15:35 -0500 Subject: [PATCH 11/18] Advances to correctly reading bytes. Something is still amiss though. Maybe I'm supposed to update the checksum? --- Machines/AmstradCPC/AmstradCPC.cpp | 11 ++++++++++- Storage/Tape/Parsers/Spectrum.hpp | 7 ++++++- Storage/Tape/Tape.cpp | 8 ++++++++ Storage/Tape/Tape.hpp | 3 +++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp index 6595fa47e..4ed228ace 100644 --- a/Machines/AmstradCPC/AmstradCPC.cpp +++ b/Machines/AmstradCPC/AmstradCPC.cpp @@ -919,12 +919,21 @@ template class ConcreteMachine: if(address == tape_read_byte_address && read_pointers_[0] == roms_[ROMType::OS].data()) { using Parser = Storage::Tape::ZXSpectrum::Parser; Parser parser(Parser::MachineType::AmstradCPC); - parser.set_cpc_read_speed(read_pointers_[tape_speed_value_address >> 14][tape_speed_value_address & 16383]); + const auto speed = read_pointers_[tape_speed_value_address >> 14][tape_speed_value_address & 16383]; + parser.set_cpc_read_speed(speed); + + // Seed with the current pulse; the CPC will have finished the + // preceding symbol and be a short way into the pulse that should determine the + // first bit of this byte. + parser.process_pulse(tape_player_.get_current_pulse()); const auto byte = parser.get_byte(tape_player_.get_tape()); auto flags = z80_.get_value_of_register(CPU::Z80::Register::Flags); if(byte) { + // In A ROM-esque fashion, begin the first pulse after the final one + // that was just consumed. + tape_player_.complete_pulse(); z80_.set_value_of_register(CPU::Z80::Register::A, *byte); flags |= CPU::Z80::Flag::Carry; } else { diff --git a/Storage/Tape/Parsers/Spectrum.hpp b/Storage/Tape/Parsers/Spectrum.hpp index 2b99fcbfb..db51c8f1b 100644 --- a/Storage/Tape/Parsers/Spectrum.hpp +++ b/Storage/Tape/Parsers/Spectrum.hpp @@ -94,6 +94,12 @@ class Parser: public Storage::Tape::PulseClassificationParser &waves) override; uint8_t checksum_ = 0; diff --git a/Storage/Tape/Tape.cpp b/Storage/Tape/Tape.cpp index 95d57346a..d2f440e1c 100644 --- a/Storage/Tape/Tape.cpp +++ b/Storage/Tape/Tape.cpp @@ -97,6 +97,14 @@ void TapePlayer::get_next_pulse() { set_next_event_time_interval(current_pulse_.length); } +Tape::Pulse TapePlayer::get_current_pulse() { + return current_pulse_; +} + +void TapePlayer::complete_pulse() { + jump_to_next_event(); +} + void TapePlayer::run_for(const Cycles cycles) { if(has_tape()) { TimedEventLoop::run_for(cycles); diff --git a/Storage/Tape/Tape.hpp b/Storage/Tape/Tape.hpp index d506e8851..005225d71 100644 --- a/Storage/Tape/Tape.hpp +++ b/Storage/Tape/Tape.hpp @@ -110,6 +110,9 @@ class TapePlayer: public TimedEventLoop, public ClockingHint::Source { ClockingHint::Preference preferred_clocking() const override; + Tape::Pulse get_current_pulse(); + void complete_pulse(); + protected: virtual void process_next_event() override; virtual void process_input_pulse(const Tape::Pulse &pulse) = 0; From 7a8317ad81e7bfcd9240a46c5a7429e9c21106db Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 12 Mar 2021 22:45:48 -0500 Subject: [PATCH 12/18] It seems a full CRC is in play. --- Machines/AmstradCPC/AmstradCPC.cpp | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp index 4ed228ace..69aeee9bb 100644 --- a/Machines/AmstradCPC/AmstradCPC.cpp +++ b/Machines/AmstradCPC/AmstradCPC.cpp @@ -32,6 +32,8 @@ #include "../../Analyser/Static/AmstradCPC/Target.hpp" +#include "../../Numeric/CRC.hpp" + #include #include #include @@ -934,6 +936,22 @@ template class ConcreteMachine: // In A ROM-esque fashion, begin the first pulse after the final one // that was just consumed. tape_player_.complete_pulse(); + + // Update in-memory CRC. + auto crc_value = + uint16_t( + read_pointers_[tape_crc_address >> 14][tape_crc_address & 16383] | + (read_pointers_[(tape_crc_address+1) >> 14][(tape_crc_address+1) & 16383] << 8) + ); + + tape_crc_.set_value(crc_value); + tape_crc_.add(*byte); + crc_value = tape_crc_.get_value(); + + write_pointers_[tape_crc_address >> 14][tape_crc_address & 16383] = uint8_t(crc_value); + write_pointers_[(tape_crc_address+1) >> 14][(tape_crc_address+1) & 16383] = uint8_t(crc_value >> 8); + + // Indicate successful byte read. z80_.set_value_of_register(CPU::Z80::Register::A, *byte); flags |= CPU::Z80::Flag::Carry; } else { @@ -1241,6 +1259,8 @@ template class ConcreteMachine: // therefore the has_fdc template flag is sufficient to locate them. static constexpr uint16_t tape_read_byte_address = has_fdc ? 0x2b20 : 0x29b0; static constexpr uint16_t tape_speed_value_address = has_fdc ? 0xb1e7 : 0xbc8f; + static constexpr uint16_t tape_crc_address = has_fdc ? 0xb1eb : 0xb8d3; + CRC::CCITT tape_crc_; HalfCycles clock_offset_; HalfCycles crtc_counter_; @@ -1258,7 +1278,7 @@ template class ConcreteMachine: ROMType upper_rom_; uint8_t *ram_pages_[4]; - uint8_t *read_pointers_[4]; + const uint8_t *read_pointers_[4]; uint8_t *write_pointers_[4]; KeyboardState key_state_; From 7d778bc32874101497d205c1c34b28605d96793d Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 12 Mar 2021 22:57:02 -0500 Subject: [PATCH 13/18] Formally introduces fast tape support as an option. It doesn't feel that fast yet though. --- Machines/AmstradCPC/AmstradCPC.cpp | 13 +++++++++++-- Machines/AmstradCPC/AmstradCPC.hpp | 9 +++++++-- .../Machine/StaticAnalyser/CSStaticAnalyser.mm | 2 +- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp index 69aeee9bb..4458cf809 100644 --- a/Machines/AmstradCPC/AmstradCPC.cpp +++ b/Machines/AmstradCPC/AmstradCPC.cpp @@ -918,7 +918,7 @@ template class ConcreteMachine: uint16_t address = cycle.address ? *cycle.address : 0x0000; switch(cycle.operation) { case CPU::Z80::PartialMachineCycle::ReadOpcode: - if(address == tape_read_byte_address && read_pointers_[0] == roms_[ROMType::OS].data()) { + if(use_fast_tape_hack_ && address == tape_read_byte_address && read_pointers_[0] == roms_[ROMType::OS].data()) { using Parser = Storage::Tape::ZXSpectrum::Parser; Parser parser(Parser::MachineType::AmstradCPC); @@ -965,8 +965,8 @@ template class ConcreteMachine: *cycle.value = 0xc9; break; } - [[fallthrough]]; + case CPU::Z80::PartialMachineCycle::Read: *cycle.value = read_pointers_[address >> 14][address & 16383]; break; @@ -1017,6 +1017,7 @@ template class ConcreteMachine: } } break; + case CPU::Z80::PartialMachineCycle::Input: // Default to nothing answering *cycle.value = 0xff; @@ -1172,12 +1173,15 @@ template class ConcreteMachine: std::unique_ptr get_options() final { auto options = std::make_unique(Configurable::OptionsType::UserFriendly); options->output = get_video_signal_configurable(); + options->quickload = allow_fast_tape_hack_; return options; } void set_options(const std::unique_ptr &str) { const auto options = dynamic_cast(str.get()); set_video_signal_configurable(options->output); + allow_fast_tape_hack_ = options->quickload; + set_use_fast_tape_hack(); } // MARK: - Joysticks @@ -1261,6 +1265,11 @@ template class ConcreteMachine: static constexpr uint16_t tape_speed_value_address = has_fdc ? 0xb1e7 : 0xbc8f; static constexpr uint16_t tape_crc_address = has_fdc ? 0xb1eb : 0xb8d3; CRC::CCITT tape_crc_; + bool use_fast_tape_hack_ = false; + bool allow_fast_tape_hack_ = false; + void set_use_fast_tape_hack() { + use_fast_tape_hack_ = allow_fast_tape_hack_ && tape_player_.has_tape(); + } HalfCycles clock_offset_; HalfCycles crtc_counter_; diff --git a/Machines/AmstradCPC/AmstradCPC.hpp b/Machines/AmstradCPC/AmstradCPC.hpp index b12a8c08a..e74d6804d 100644 --- a/Machines/AmstradCPC/AmstradCPC.hpp +++ b/Machines/AmstradCPC/AmstradCPC.hpp @@ -29,12 +29,17 @@ class Machine { static Machine *AmstradCPC(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); /// Defines the runtime options available for an Amstrad CPC. - class Options: public Reflection::StructImpl, public Configurable::DisplayOption { + class Options: public Reflection::StructImpl, public Configurable::DisplayOption, public Configurable::QuickloadOption { friend Configurable::DisplayOption; + friend Configurable::QuickloadOption; public: - Options(Configurable::OptionsType) : Configurable::DisplayOption(Configurable::Display::RGB) { + Options(Configurable::OptionsType type) : + Configurable::DisplayOption(Configurable::Display::RGB), + Configurable::QuickloadOption(type == Configurable::OptionsType::UserFriendly) + { if(needs_declare()) { declare_display_option(); + declare_quickload_option(); limit_enum(&output, Configurable::Display::RGB, Configurable::Display::CompositeColour, -1); } } diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm index fa0ccf6e3..78317d352 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm @@ -250,7 +250,7 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K - (NSString *)optionsPanelNibName { switch(_targets.front()->machine) { - case Analyser::Machine::AmstradCPC: return @"CompositeOptions"; + case Analyser::Machine::AmstradCPC: return @"QuickLoadCompositeOptions"; case Analyser::Machine::AppleII: return @"AppleIIOptions"; case Analyser::Machine::Atari2600: return @"Atari2600Options"; case Analyser::Machine::AtariST: return @"CompositeOptions"; From 54e2eb0948355bba35bee33355793b384eaa5004 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 12 Mar 2021 23:04:45 -0500 Subject: [PATCH 14/18] Shortens wasted typing. --- Analyser/Static/AmstradCPC/StaticAnalyser.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Analyser/Static/AmstradCPC/StaticAnalyser.cpp b/Analyser/Static/AmstradCPC/StaticAnalyser.cpp index 915275afe..500e3ce55 100644 --- a/Analyser/Static/AmstradCPC/StaticAnalyser.cpp +++ b/Analyser/Static/AmstradCPC/StaticAnalyser.cpp @@ -223,7 +223,7 @@ Analyser::Static::TargetList Analyser::Static::AmstradCPC::GetTargets(const Medi // 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"; + target->loading_command = "|tape\nrun\"\n123"; } } From d368dae94ace214d0d513a2d8a061732c43aeae7 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 12 Mar 2021 23:09:51 -0500 Subject: [PATCH 15/18] Adds tape motor LED. --- Machines/AmstradCPC/AmstradCPC.cpp | 1 + Storage/Tape/Tape.cpp | 12 ++++++++++++ Storage/Tape/Tape.hpp | 6 ++++++ 3 files changed, 19 insertions(+) diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp index 4458cf809..4cb3f0d34 100644 --- a/Machines/AmstradCPC/AmstradCPC.cpp +++ b/Machines/AmstradCPC/AmstradCPC.cpp @@ -1167,6 +1167,7 @@ template class ConcreteMachine: // MARK: - Activity Source void set_activity_observer([[maybe_unused]] Activity::Observer *observer) final { if constexpr (has_fdc) fdc_.set_activity_observer(observer); + tape_player_.set_activity_observer(observer); } // MARK: - Configuration options. diff --git a/Storage/Tape/Tape.cpp b/Storage/Tape/Tape.cpp index d2f440e1c..9dcfa7165 100644 --- a/Storage/Tape/Tape.cpp +++ b/Storage/Tape/Tape.cpp @@ -135,6 +135,18 @@ void BinaryTapePlayer::set_motor_control(bool enabled) { if(motor_is_running_ != enabled) { motor_is_running_ = enabled; update_clocking_observer(); + + if(observer_) { + observer_->set_led_status("Tape motor", enabled); + } + } +} + +void BinaryTapePlayer::set_activity_observer(Activity::Observer *observer) { + observer_ = observer; + if(observer) { + observer->register_led("Tape motor"); + observer_->set_led_status("Tape motor", motor_is_running_); } } diff --git a/Storage/Tape/Tape.hpp b/Storage/Tape/Tape.hpp index 005225d71..19e5bffbb 100644 --- a/Storage/Tape/Tape.hpp +++ b/Storage/Tape/Tape.hpp @@ -16,6 +16,8 @@ #include "../TimedEventLoop.hpp" +#include "../../Activity/Source.hpp" + namespace Storage { namespace Tape { @@ -151,11 +153,15 @@ class BinaryTapePlayer : public TapePlayer { ClockingHint::Preference preferred_clocking() const final; + void set_activity_observer(Activity::Observer *observer); + protected: Delegate *delegate_ = nullptr; void process_input_pulse(const Storage::Tape::Tape::Pulse &pulse) final; bool input_level_ = false; bool motor_is_running_ = false; + + Activity::Observer *observer_ = nullptr; }; } From 1a5dafae0060e43db27cd69679e406fa086c31d2 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 15 Mar 2021 11:37:03 -0400 Subject: [PATCH 16/18] Slightly neatens. --- Machines/AmstradCPC/AmstradCPC.hpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Machines/AmstradCPC/AmstradCPC.hpp b/Machines/AmstradCPC/AmstradCPC.hpp index e74d6804d..ae1cf1015 100644 --- a/Machines/AmstradCPC/AmstradCPC.hpp +++ b/Machines/AmstradCPC/AmstradCPC.hpp @@ -29,7 +29,11 @@ class Machine { static Machine *AmstradCPC(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); /// Defines the runtime options available for an Amstrad CPC. - class Options: public Reflection::StructImpl, public Configurable::DisplayOption, public Configurable::QuickloadOption { + class Options: + public Reflection::StructImpl, + public Configurable::DisplayOption, + public Configurable::QuickloadOption + { friend Configurable::DisplayOption; friend Configurable::QuickloadOption; public: From 397704a1e60f7e6feb66372ba08c1e4a617383a5 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 15 Mar 2021 11:37:23 -0400 Subject: [PATCH 17/18] Withdraws published quick-load option for the CPC. --- .../Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm index 78317d352..48950edca 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm @@ -250,7 +250,8 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K - (NSString *)optionsPanelNibName { switch(_targets.front()->machine) { - case Analyser::Machine::AmstradCPC: return @"QuickLoadCompositeOptions"; +// case Analyser::Machine::AmstradCPC: return @"QuickLoadCompositeOptions"; + case Analyser::Machine::AmstradCPC: return @"CompositeOptions"; case Analyser::Machine::AppleII: return @"AppleIIOptions"; case Analyser::Machine::Atari2600: return @"Atari2600Options"; case Analyser::Machine::AtariST: return @"CompositeOptions"; From cdc19c699001cb67217a72bb91554b9d75214dcb Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 15 Mar 2021 11:39:15 -0400 Subject: [PATCH 18/18] Adds TODO. --- Machines/AmstradCPC/AmstradCPC.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp index 4cb3f0d34..a5aac6294 100644 --- a/Machines/AmstradCPC/AmstradCPC.cpp +++ b/Machines/AmstradCPC/AmstradCPC.cpp @@ -918,6 +918,10 @@ template class ConcreteMachine: uint16_t address = cycle.address ? *cycle.address : 0x0000; switch(cycle.operation) { case CPU::Z80::PartialMachineCycle::ReadOpcode: + + // TODO: just capturing byte reads as below doesn't seem to do that much in terms of acceleration; + // I'm not immediately clear whether that's just because the machine still has to sit through + // pilot tone in real time, or just that almost no software uses the ROM loader. if(use_fast_tape_hack_ && address == tape_read_byte_address && read_pointers_[0] == roms_[ROMType::OS].data()) { using Parser = Storage::Tape::ZXSpectrum::Parser; Parser parser(Parser::MachineType::AmstradCPC);