diff --git a/Storage/Tape/Formats/ZXSpectrumTAP.cpp b/Storage/Tape/Formats/ZXSpectrumTAP.cpp
index 549c9ba2b..d97341ad2 100644
--- a/Storage/Tape/Formats/ZXSpectrumTAP.cpp
+++ b/Storage/Tape/Formats/ZXSpectrumTAP.cpp
@@ -10,6 +10,14 @@
 
 using namespace Storage::Tape;
 
+/*
+	The understanding of idiomatic Spectrum data encoding below
+	is taken from the TZX specifications at
+	https://worldofspectrum.net/features/TZXformat.html ;
+	specifics of the TAP encoding were gained from
+	https://sinclair.wiki.zxnet.co.uk/wiki/TAP_format
+*/
+
 ZXSpectrumTAP::ZXSpectrumTAP(const std::string &file_name) :
 	file_(file_name)
 {
@@ -34,14 +42,85 @@ ZXSpectrumTAP::ZXSpectrumTAP(const std::string &file_name) :
 }
 
 bool ZXSpectrumTAP::is_at_end() {
-	return false;
+	return file_.tell() == file_.stats().st_size;
 }
 
 void ZXSpectrumTAP::virtual_reset() {
 	file_.seek(0, SEEK_SET);
-	block_length_ = file_.get16le();
+	read_next_block();
 }
 
 Tape::Pulse ZXSpectrumTAP::virtual_get_next_pulse() {
-	return Pulse();
+	// Adopt a general pattern of high then low.
+	Pulse pulse;
+	pulse.type = (distance_into_phase_ & 1) ? Pulse::Type::High : Pulse::Type::Low;
+
+	switch(phase_) {
+		default: break;
+
+		case Phase::PilotTone: {
+			// Output: pulses of length 2168;
+			// 8063 pulses if block type is 0, otherwise 3223;
+			// then a 667-length pulse followed by a 735-length pulse.
+
+			pulse.length = Time(271, 437'500);	// i.e. 2168 / 3'500'000
+			++distance_into_phase_;
+
+			// Check whether in the last two.
+			if(distance_into_phase_ >= (block_type_ ? 8063 : 3223)) {
+				pulse.length = (distance_into_phase_ & 1) ? Time(667, 3'500'000) : Time(735, 3'500'000);
+
+				// Check whether this is the last one.
+				if(distance_into_phase_ == (block_type_ ? 8064 : 3224)) {
+					distance_into_phase_ = 0;
+					phase_ = Phase::Data;
+				}
+			}
+		} break;
+
+		case Phase::Data: {
+			// Output two pulses of length 855 for a 0; two of length 1710 for a 1,
+			// from MSB to LSB.
+			pulse.length = (data_byte_ & 0x80) ? Time(1710, 3'500'000) : Time(855, 3'500'000);
+			++distance_into_phase_;
+
+			if(!(distance_into_phase_ & 1)) {
+				data_byte_ <<= 1;
+			}
+
+			if(!(distance_into_phase_ & 15)) {
+				if((distance_into_phase_ >> 4) == block_length_) {
+					if(block_type_) {
+						distance_into_phase_ = 0;
+						phase_ = Phase::Gap;
+					} else {
+						read_next_block();
+					}
+				} else {
+					data_byte_ = file_.get8();
+				}
+			}
+		} break;
+
+		case Phase::Gap:
+			Pulse gap;
+			gap.type = Pulse::Type::Zero;
+			gap.length = Time(1);
+
+			read_next_block();
+		return gap;
+	}
+
+	return pulse;
+}
+
+void ZXSpectrumTAP::read_next_block() {
+	if(is_at_end()) {
+		phase_ = Phase::Gap;
+	} else {
+		block_length_ = file_.get16le();
+		data_byte_ = block_type_ = file_.get8();
+		phase_ = Phase::PilotTone;
+	}
+	distance_into_phase_ = 0;
 }
diff --git a/Storage/Tape/Formats/ZXSpectrumTAP.hpp b/Storage/Tape/Formats/ZXSpectrumTAP.hpp
index 5a84d909c..ec1cc035c 100644
--- a/Storage/Tape/Formats/ZXSpectrumTAP.hpp
+++ b/Storage/Tape/Formats/ZXSpectrumTAP.hpp
@@ -39,6 +39,15 @@ class ZXSpectrumTAP: public Tape {
 		Storage::FileHolder file_;
 
 		uint16_t block_length_ = 0;
+		uint8_t block_type_ = 0;
+		uint8_t data_byte_ = 0;
+		enum Phase {
+			PilotTone,
+			Data,
+			Gap
+		} phase_ = Phase::PilotTone;
+		int distance_into_phase_ = 0;
+		void read_next_block();
 
 		// Implemented to satisfy @c Tape.
 		bool is_at_end() override;