From d93d380c888c16735a6a79ffd9ce9a1869dc58da Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
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 = "<group>"; };
 		4B595FAB2086DFBA0083CAA8 /* AudioToggle.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = AudioToggle.hpp; sourceTree = "<group>"; };
 		4B595FAC2086DFBA0083CAA8 /* AudioToggle.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AudioToggle.cpp; sourceTree = "<group>"; };
+		4B5D5C9525F56FC7001B4623 /* Spectrum.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Spectrum.cpp; path = Parsers/Spectrum.cpp; sourceTree = "<group>"; };
+		4B5D5C9625F56FC7001B4623 /* Spectrum.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Spectrum.hpp; path = Parsers/Spectrum.hpp; sourceTree = "<group>"; };
 		4B5FADB81DE3151600AEC565 /* FileHolder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = FileHolder.cpp; sourceTree = "<group>"; };
 		4B5FADB91DE3151600AEC565 /* FileHolder.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = FileHolder.hpp; sourceTree = "<group>"; };
 		4B5FADBE1DE3BF2B00AEC565 /* Microdisc.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Microdisc.cpp; path = Oric/Microdisc.cpp; sourceTree = "<group>"; };
@@ -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<float>() * 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<Storage::Tape::ZXSpectrum::WaveType> &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<WaveType, SymbolType> {
+	private:
+		void process_pulse(const Storage::Tape::Tape::Pulse &pulse) override;
+		void inspect_waves(const std::vector<WaveType> &waves) override;
+};
+
+}
+}
+}
+
+#endif /* Spectrum_hpp */

From 40516c9cece8a037e09047a8cd548709797162e8 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
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<Storage::Tape::Tape> &tape) {
 }
 
 int Parser::get_next_byte(const std::shared_ptr<Storage::Tape::Tape> &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<float>())));
 
-	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<SymbolType>, 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<uint16_t, 0x0000, 0x0000, false, false> 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<Header> Parser::get_next_header_body(const std::shared_ptr<Stora
 	reset_parity_byte();
 
 	// get header type
-	uint8_t header_type = get_next_byte(tape);
+	const uint8_t header_type = get_next_byte(tape);
 	switch(header_type) {
 		default:	header->type = Header::Unknown;					break;
 		case 0x01:	header->type = Header::RelocatableProgram;		break;
@@ -92,7 +92,7 @@ std::unique_ptr<Header> Parser::get_next_header_body(const std::shared_ptr<Stora
 		header->data.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<Header> Parser::get_next_header_body(const std::shared_ptr<Stora
 	return header;
 }
 
-void Header::serialise(uint8_t *target, uint16_t length) {
+void Header::serialise(uint8_t *target, [[maybe_unused]] uint16_t length) {
 	switch(type) {
 		default:							target[0] = 0xff;	break;
 		case Header::RelocatableProgram:	target[0] = 0x01;	break;
@@ -121,7 +121,6 @@ void Header::serialise(uint8_t *target, uint16_t length) {
 	}
 
 	// TODO: validate length.
-	(void)length;
 
 	std::memcpy(&target[1], data.data(), 191);
 }
@@ -178,7 +177,7 @@ void Parser::proceed_to_landing_zone(const std::shared_ptr<Storage::Tape::Tape>
 */
 void Parser::proceed_to_symbol(const std::shared_ptr<Storage::Tape::Tape> &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<Storage::Tape::Tape> &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<Storage::Tape::Tape> &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<WaveType, SymbolTy
 			indicates a high to low transition, inspects the time since the last transition, to produce
 			a long, medium, short or unrecognised wave period.
 		*/
-		void process_pulse(const Storage::Tape::Tape::Pulse &pulse);
+		void process_pulse(const Storage::Tape::Tape::Pulse &pulse) override;
 		bool previous_was_high_ = false;
 		float wave_period_ = 0.0f;
 
@@ -134,7 +134,7 @@ class Parser: public Storage::Tape::PulseClassificationParser<WaveType, SymbolTy
 			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 inspect_waves(const std::vector<WaveType> &waves);
+		void inspect_waves(const std::vector<WaveType> &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<WaveType, SymbolTy
 		bool sync_and_get_encoding_speed(const std::shared_ptr<Storage::Tape::Tape> &tape);
 
 	private:
-		void process_pulse(const Storage::Tape::Tape::Pulse &pulse);
-		void inspect_waves(const std::vector<WaveType> &waves);
+		void process_pulse(const Storage::Tape::Tape::Pulse &pulse) override;
+		void inspect_waves(const std::vector<WaveType> &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<float>();
+	const auto pulse_time = pulse_time_.get<float>();
 
 	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<WaveType, SymbolTy
 		Time pulse_time_;
 		void post_pulse();
 
-		void process_pulse(const Storage::Tape::Tape::Pulse &pulse);
-		void mark_end();
-
-		void inspect_waves(const std::vector<WaveType> &waves);
+		void process_pulse(const Storage::Tape::Tape::Pulse &pulse) override;
+		void mark_end() override;
+		void inspect_waves(const std::vector<WaveType> &waves) override;
 
 		std::shared_ptr<std::vector<uint8_t>> get_next_file_data(const std::shared_ptr<Storage::Tape::Tape> &tape);
 };

From ab5e4ca9c70969720de661c702b4c6955b25173e Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
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<Data> Parser::get_next_data_body(const std::shared_ptr<Storage::
 
 	// accumulate until the next non-word marker is hit
 	while(!tape->is_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<Storage::Tape::Tape>
 	}
 }
 
-/*!
-	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<Storage::Tape::Tape> &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<Storage::Tape::Tape
 	int byte_plus_parity = 0;
 	int c = 9;
 	while(c--) {
-		SymbolType next_symbol = get_next_symbol(tape);
+		const SymbolType next_symbol = get_next_symbol(tape);
 		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);
 	}
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<WaveType, SymbolTy
 		*/
 		void proceed_to_landing_zone(const std::shared_ptr<Storage::Tape::Tape> &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<Storage::Tape::Tape> &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 <typename SymbolType> 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<Storage::Tape::Tape> &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 <thomas.harte@gmail.com>
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<Storage::Tape::ZXSpectrum::WaveType
 		break;
 	}
 }
+
+std::optional<Header> Parser::find_header(const std::shared_ptr<Storage::Tape::Tape> &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<uint8_t> Parser::get_byte(const std::shared_ptr<Storage::Tape::Tape> &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 <optional>
+
 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<uint16_t> 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<WaveType, SymbolType> {
+	public:
+		/*!
+			Finds the next header from the tape, if any.
+		*/
+		std::optional<Header> find_header(const std::shared_ptr<Storage::Tape::Tape> &tape);
+
+		void reset_checksum();
+		std::optional<uint8_t> get_byte(const std::shared_ptr<Storage::Tape::Tape> &tape);
+
 	private:
 		void process_pulse(const Storage::Tape::Tape::Pulse &pulse) override;
 		void inspect_waves(const std::vector<WaveType> &waves) override;
+
+		uint8_t checksum_ = 0;
 };
 
 }

From 5c90744f0c448b02034e4a56c31b11573999d8cf Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
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<Storage::Tape::Tape> &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<Storage::Tape::Tape> &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<SymbolType>, public Shifter::Delegate
 		unsigned int get_next_short(const std::shared_ptr<Storage::Tape::Tape> &tape);
 		unsigned int get_next_word(const std::shared_ptr<Storage::Tape::Tape> &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<WaveType, SymbolTy
 		bool wave_was_high_;
 		float cycle_length_;
 
-		struct Pattern
-		{
+		struct Pattern {
 			WaveType type;
 			int count = 0;
 		};

From f9852489023bcf451f1827b7715804569fe9442d Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
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 <cstring>
+
 //
 // 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 <thomas.harte@gmail.com>
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<Header> Parser::find_header(const std::shared_ptr<Storage::Tape::T
 	// Read market 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();
 

From f190a1395ae32775b0d871ddf990333fde98131e Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
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<Analyser::Static::AmstradCPC::Target> &target) {
 
@@ -155,7 +158,7 @@ static void InspectCatalogue(
 	target->loading_command = "cat\n";
 }
 
-static bool CheckBootSector(const std::shared_ptr<Storage::Disk::Disk> &disk, const std::unique_ptr<Analyser::Static::AmstradCPC::Target> &target) {
+bool CheckBootSector(const std::shared_ptr<Storage::Disk::Disk> &disk, const std::unique_ptr<Analyser::Static::AmstradCPC::Target> &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<Storage::Disk::Disk> &disk, co
 	return false;
 }
 
+bool IsAmstradTape(const std::shared_ptr<Storage::Tape::Tape> &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<Target>();
@@ -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 <typename IntType, IntType reset_value, IntType output_xor, bool reflect_input, bool reflect_output> class Generator {
 	public:
@@ -90,18 +102,6 @@ template <typename IntType, IntType reset_value, IntType output_xor, bool reflec
 		static constexpr int multibyte_shift = (sizeof(IntType) * 8) - 8;
 		IntType xor_table[256];
 		IntType value_;
-
-		constexpr uint8_t reverse_byte(uint8_t byte) const {
-			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);
-		}
 };
 
 /*!
diff --git a/Storage/Tape/Parsers/Spectrum.cpp b/Storage/Tape/Parsers/Spectrum.cpp
index 26224d456..bcf18194c 100644
--- a/Storage/Tape/Parsers/Spectrum.cpp
+++ b/Storage/Tape/Parsers/Spectrum.cpp
@@ -8,14 +8,23 @@
 
 #include "Spectrum.hpp"
 
+#include "../../../Numeric/CRC.hpp"
+
 #include <cstring>
 
 //
-// 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<float>() * 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<Storage::Tape::ZXSpectrum::WaveType> &waves) {
@@ -71,21 +138,6 @@ void Parser::inspect_waves(const std::vector<Storage::Tape::ZXSpectrum::WaveType
 		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:
@@ -99,50 +151,45 @@ void Parser::inspect_waves(const std::vector<Storage::Tape::ZXSpectrum::WaveType
 	}
 }
 
-std::optional<Header> Parser::find_header(const std::shared_ptr<Storage::Tape::Tape> &tape) {
+std::optional<Block> Parser::find_block(const std::shared_ptr<Storage::Tape::Tape> &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<uint8_t> Parser::get_block_body(const std::shared_ptr<Storage::Tape::Tape> &tape) {
+	std::vector<uint8_t> 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<uint8_t> Parser::get_byte(const std::shared_ptr<Storage::Tape::Tape> &tape) {
@@ -152,6 +199,11 @@ std::optional<uint8_t> Parser::get_byte(const std::shared_ptr<Storage::Tape::Tap
 		if(symbol != SymbolType::One && symbol != SymbolType::Zero) return std::nullopt;
 		result = uint8_t((result << 1) | (symbol == SymbolType::One));
 	}
+
+	if(should_flip_bytes()) {
+		result = CRC::reverse_byte(result);
+	}
+
 	checksum_ ^= result;
 	return result;
 }
diff --git a/Storage/Tape/Parsers/Spectrum.hpp b/Storage/Tape/Parsers/Spectrum.hpp
index aa2c0bb34..e8a2d5b32 100644
--- a/Storage/Tape/Parsers/Spectrum.hpp
+++ b/Storage/Tape/Parsers/Spectrum.hpp
@@ -11,7 +11,9 @@
 
 #include "TapeParser.hpp"
 
+#include <array>
 #include <optional>
+#include <vector>
 
 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<uint16_t> 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<WaveType, SymbolType> {
 	public:
-		/*!
-			Finds the next header from the tape, if any.
-		*/
-		std::optional<Header> find_header(const std::shared_ptr<Storage::Tape::Tape> &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<Block> find_block(const std::shared_ptr<Storage::Tape::Tape> &tape);
+
+		/*!
+			Reads the contents of the rest of this block, until the next gap.
+		*/
+		std::vector<uint8_t> get_block_body(const std::shared_ptr<Storage::Tape::Tape> &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<uint8_t> get_byte(const std::shared_ptr<Storage::Tape::Tape> &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<WaveType> &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<float, 8> calibration_pulses_;
+		size_t calibration_pulse_pointer_ = 0;
 };
 
 }

From cd215ef52121119c657f089721e0e8a4ee5c8547 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
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 <bool has_fdc> 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 <bool has_fdc> 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 <thomas.harte@gmail.com>
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<Storage::Tape::ZXSpectrum::WaveType> &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<WaveType, SymbolTy
 		};
 		Parser(MachineType);
 
+		/*!
+			Calibrates the expected data speed using a value in the CPC's native tape-speed measurement scale.
+		*/
+		void set_cpc_read_speed(uint8_t);
+
 		/*!
 			Finds the next block from the tape, if any.
 
@@ -117,6 +122,8 @@ class Parser: public Storage::Tape::PulseClassificationParser<WaveType, SymbolTy
 
 		std::array<float, 8> 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 <thomas.harte@gmail.com>
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 <bool has_fdc> 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<WaveType, SymbolTy
 		*/
 		void seed_checksum(uint8_t value = 0x00);
 
+		/*!
+			Push a pulse; primarily provided for Storage::Tape::PulseClassificationParser but also potentially useful
+			for picking up fast loading from an ongoing tape.
+		*/
+		void process_pulse(const Storage::Tape::Tape::Pulse &pulse) override;
+
 	private:
 		const MachineType machine_type_;
 		constexpr bool should_flip_bytes() {
@@ -103,7 +109,6 @@ class Parser: public Storage::Tape::PulseClassificationParser<WaveType, SymbolTy
 			return machine_type_ != MachineType::ZXSpectrum;
 		}
 
-		void process_pulse(const Storage::Tape::Tape::Pulse &pulse) override;
 		void inspect_waves(const std::vector<WaveType> &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 <thomas.harte@gmail.com>
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 <array>
 #include <cstdint>
 #include <vector>
@@ -934,6 +936,22 @@ template <bool has_fdc> 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 <bool has_fdc> 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 <bool has_fdc> 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 <thomas.harte@gmail.com>
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 <bool has_fdc> 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 <bool has_fdc> 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 <bool has_fdc> class ConcreteMachine:
 						}
 					}
 				break;
+
 				case CPU::Z80::PartialMachineCycle::Input:
 					// Default to nothing answering
 					*cycle.value = 0xff;
@@ -1172,12 +1173,15 @@ template <bool has_fdc> class ConcreteMachine:
 		std::unique_ptr<Reflection::Struct> get_options() final {
 			auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
 			options->output = get_video_signal_configurable();
+			options->quickload = allow_fast_tape_hack_;
 			return options;
 		}
 
 		void set_options(const std::unique_ptr<Reflection::Struct> &str) {
 			const auto options = dynamic_cast<Options *>(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 <bool has_fdc> 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<Options>, public Configurable::DisplayOption<Options> {
+		class Options: public Reflection::StructImpl<Options>, public Configurable::DisplayOption<Options>, public Configurable::QuickloadOption<Options> {
 			friend Configurable::DisplayOption<Options>;
+			friend Configurable::QuickloadOption<Options>;
 			public:
-				Options(Configurable::OptionsType) : Configurable::DisplayOption<Options>(Configurable::Display::RGB)  {
+				Options(Configurable::OptionsType type) :
+					Configurable::DisplayOption<Options>(Configurable::Display::RGB),
+					Configurable::QuickloadOption<Options>(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 <thomas.harte@gmail.com>
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 <thomas.harte@gmail.com>
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 <bool has_fdc> 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 <thomas.harte@gmail.com>
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<Options>, public Configurable::DisplayOption<Options>, public Configurable::QuickloadOption<Options> {
+		class Options:
+			public Reflection::StructImpl<Options>,
+			public Configurable::DisplayOption<Options>,
+			public Configurable::QuickloadOption<Options>
+		{
 			friend Configurable::DisplayOption<Options>;
 			friend Configurable::QuickloadOption<Options>;
 			public:

From 397704a1e60f7e6feb66372ba08c1e4a617383a5 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
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 <thomas.harte@gmail.com>
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 <bool has_fdc> 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);