From 6c606b5506a36127271f1da6b1e85413a126c77f Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 25 Dec 2021 17:06:12 -0500
Subject: [PATCH 01/25] Fix through route to
 `TargetPlatform::TypeDistinguisher`.

---
 Storage/Disk/DiskImage/DiskImage.hpp | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/Storage/Disk/DiskImage/DiskImage.hpp b/Storage/Disk/DiskImage/DiskImage.hpp
index ed051756f..d12d4709a 100644
--- a/Storage/Disk/DiskImage/DiskImage.hpp
+++ b/Storage/Disk/DiskImage/DiskImage.hpp
@@ -14,6 +14,7 @@
 
 #include "../Disk.hpp"
 #include "../Track/Track.hpp"
+#include "../../TargetPlatforms.hpp"
 
 namespace Storage {
 namespace Disk {
@@ -86,8 +87,11 @@ class DiskImageHolderBase: public Disk {
 	Provides a wrapper that wraps a DiskImage to make it into a Disk, providing caching and,
 	thereby, an intermediate store for modified tracks so that mutable disk images can either
 	update on the fly or perform a block update on closure, as appropriate.
+
+	Implements TargetPlatform::TypeDistinguisher to return either no information whatsoever, if
+	the underlying image doesn't implement TypeDistinguisher, or else to pass the call along.
 */
-template <typename T> class DiskImageHolder: public DiskImageHolderBase {
+template <typename T> class DiskImageHolder: public DiskImageHolderBase, public TargetPlatform::TypeDistinguisher {
 	public:
 		template <typename... Ts> DiskImageHolder(Ts&&... args) :
 			disk_image_(args...) {}
@@ -103,6 +107,14 @@ template <typename T> class DiskImageHolder: public DiskImageHolderBase {
 
 	private:
 		T disk_image_;
+
+		TargetPlatform::Type target_platform_type() final {
+			if constexpr (std::is_base_of<TargetPlatform::TypeDistinguisher, T>::value) {
+				return static_cast<TargetPlatform::TypeDistinguisher *>(&disk_image_)->target_platform_type();
+			} else {
+				return TargetPlatform::Type(~0);
+			}
+		}
 };
 
 #include "DiskImageImplementation.hpp"

From dba3a3d9426cf45cc92ee2939fda43b28ec1a0f8 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 25 Dec 2021 17:06:47 -0500
Subject: [PATCH 02/25] Add through route to an IPF container.

---
 Analyser/Static/StaticAnalyser.cpp            | 13 +++--
 .../Clock Signal.xcodeproj/project.pbxproj    |  8 +++
 OSBindings/Mac/Clock Signal/Info.plist        | 20 +++++++
 Storage/Disk/DiskImage/Formats/IPF.cpp        | 27 +++++++++
 Storage/Disk/DiskImage/Formats/IPF.hpp        | 56 +++++++++++++++++++
 5 files changed, 120 insertions(+), 4 deletions(-)
 create mode 100644 Storage/Disk/DiskImage/Formats/IPF.cpp
 create mode 100644 Storage/Disk/DiskImage/Formats/IPF.hpp

diff --git a/Analyser/Static/StaticAnalyser.cpp b/Analyser/Static/StaticAnalyser.cpp
index fb0c1b7a5..99668d6e1 100644
--- a/Analyser/Static/StaticAnalyser.cpp
+++ b/Analyser/Static/StaticAnalyser.cpp
@@ -43,11 +43,12 @@
 #include "../../Storage/Disk/DiskImage/Formats/AppleDSK.hpp"
 #include "../../Storage/Disk/DiskImage/Formats/CPCDSK.hpp"
 #include "../../Storage/Disk/DiskImage/Formats/D64.hpp"
-#include "../../Storage/Disk/DiskImage/Formats/MacintoshIMG.hpp"
 #include "../../Storage/Disk/DiskImage/Formats/G64.hpp"
 #include "../../Storage/Disk/DiskImage/Formats/DMK.hpp"
 #include "../../Storage/Disk/DiskImage/Formats/FAT12.hpp"
 #include "../../Storage/Disk/DiskImage/Formats/HFE.hpp"
+#include "../../Storage/Disk/DiskImage/Formats/IPF.hpp"
+#include "../../Storage/Disk/DiskImage/Formats/MacintoshIMG.hpp"
 #include "../../Storage/Disk/DiskImage/Formats/MSA.hpp"
 #include "../../Storage/Disk/DiskImage/Formats/NIB.hpp"
 #include "../../Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp"
@@ -103,8 +104,8 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
 #define InsertInstance(list, instance, platforms) \
 	list.emplace_back(instance);\
 	potential_platforms |= platforms;\
-	TargetPlatform::TypeDistinguisher *distinguisher = dynamic_cast<TargetPlatform::TypeDistinguisher *>(list.back().get());\
-	if(distinguisher) potential_platforms &= distinguisher->target_platform_type(); \
+	TargetPlatform::TypeDistinguisher *const distinguisher = dynamic_cast<TargetPlatform::TypeDistinguisher *>(list.back().get());\
+	if(distinguisher) potential_platforms &= distinguisher->target_platform_type();
 
 #define Insert(list, class, platforms, ...) \
 	InsertInstance(list, new Storage::class(__VA_ARGS__), platforms);
@@ -161,7 +162,11 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
 			// HFE (TODO: switch to AllDisk once the MSX stops being so greedy)
 	Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)		// IMG (DiskCopy 4.2)
 	Format("image", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh)	// IMG (DiskCopy 4.2)
-	Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::FAT12>, TargetPlatform::Enterprise)		// IMG (Enterprise/MS-DOS style)
+	Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::FAT12>, TargetPlatform::Enterprise)			// IMG (Enterprise/MS-DOS style)
+	Format(	"ipf",
+			result.disks,
+			Disk::DiskImageHolder<Storage::Disk::IPF>,
+			TargetPlatform::Amiga | TargetPlatform::AtariST | TargetPlatform::AmstradCPC | TargetPlatform::ZXSpectrum)		// IPF
 	Format("msa", result.disks, Disk::DiskImageHolder<Storage::Disk::MSA>, TargetPlatform::AtariST)				// MSA
 	Format("nib", result.disks, Disk::DiskImageHolder<Storage::Disk::NIB>, TargetPlatform::DiskII)				// NIB
 	Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081)											// O
diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj
index 7962a7be7..734e8c3cb 100644
--- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
+++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
@@ -273,6 +273,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 */; };
+		4B5B37312777C7FC0047F238 /* IPF.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5B372F2777C7FC0047F238 /* IPF.cpp */; };
+		4B5B37322777C7FC0047F238 /* IPF.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5B372F2777C7FC0047F238 /* IPF.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 */; };
@@ -1350,6 +1352,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>"; };
+		4B5B372F2777C7FC0047F238 /* IPF.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = IPF.cpp; sourceTree = "<group>"; };
+		4B5B37302777C7FC0047F238 /* IPF.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = IPF.hpp; 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>"; };
@@ -2788,6 +2792,7 @@
 				4BEBFB4B2002C4BF000708CC /* FAT12.cpp */,
 				4B4518931F75FD1B00926311 /* G64.cpp */,
 				4B4518951F75FD1B00926311 /* HFE.cpp */,
+				4B5B372F2777C7FC0047F238 /* IPF.cpp */,
 				4BB4BFAE22A42F290069048D /* MacintoshIMG.cpp */,
 				4B58601C1F806AB200AEE2E3 /* MFMSectorDump.cpp */,
 				4BC131782346DF2B00E4FF3D /* MSA.cpp */,
@@ -2807,6 +2812,7 @@
 				4BEBFB4C2002C4BF000708CC /* FAT12.hpp */,
 				4B4518941F75FD1B00926311 /* G64.hpp */,
 				4B4518961F75FD1B00926311 /* HFE.hpp */,
+				4B5B37302777C7FC0047F238 /* IPF.hpp */,
 				4BB4BFAF22A42F290069048D /* MacintoshIMG.hpp */,
 				4B58601D1F806AB200AEE2E3 /* MFMSectorDump.hpp */,
 				4BC131792346DF2B00E4FF3D /* MSA.hpp */,
@@ -5421,6 +5427,7 @@
 				4BC131772346DE9100E4FF3D /* StaticAnalyser.cpp in Sources */,
 				4B055ACF1FAE9B030060FFFF /* SoundGenerator.cpp in Sources */,
 				4B4DEC08252BFA56004583AC /* 65816Base.cpp in Sources */,
+				4B5B37322777C7FC0047F238 /* IPF.cpp in Sources */,
 				4B894519201967B4007DE474 /* ConfidenceCounter.cpp in Sources */,
 				4B055AEE1FAE9BBF0060FFFF /* Keyboard.cpp in Sources */,
 				4B055AED1FAE9BA20060FFFF /* Z80Storage.cpp in Sources */,
@@ -5574,6 +5581,7 @@
 				4B89451E201967B4007DE474 /* Tape.cpp in Sources */,
 				4BAF2B4E2004580C00480230 /* DMK.cpp in Sources */,
 				4BB697CE1D4BA44400248BDF /* CommodoreGCR.cpp in Sources */,
+				4B5B37312777C7FC0047F238 /* IPF.cpp in Sources */,
 				4B0ACC3023775819008902D0 /* TIASound.cpp in Sources */,
 				4B7136861F78724F008B8ED9 /* Encoder.cpp in Sources */,
 				4B0E04EA1FC9E5DA00F43484 /* CAS.cpp in Sources */,
diff --git a/OSBindings/Mac/Clock Signal/Info.plist b/OSBindings/Mac/Clock Signal/Info.plist
index c0ae89362..635d364c9 100644
--- a/OSBindings/Mac/Clock Signal/Info.plist	
+++ b/OSBindings/Mac/Clock Signal/Info.plist	
@@ -652,6 +652,26 @@
 			<key>NSDocumentClass</key>
 			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
 		</dict>
+		<dict>
+			<key>CFBundleTypeExtensions</key>
+			<array>
+				<string>ipf</string>
+			</array>
+			<key>CFBundleTypeName</key>
+			<string>Software Preservation Society Disk Image</string>
+			<key>CFBundleTypeOSTypes</key>
+			<array>
+				<string>????</string>
+			</array>
+			<key>CFBundleTypeRole</key>
+			<string>Viewer</string>
+			<key>LSHandlerRank</key>
+			<string>Owner</string>
+			<key>LSTypeIsPackage</key>
+			<false/>
+			<key>NSDocumentClass</key>
+			<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
+		</dict>
 	</array>
 	<key>CFBundleExecutable</key>
 	<string>$(EXECUTABLE_NAME)</string>
diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
new file mode 100644
index 000000000..44999490e
--- /dev/null
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -0,0 +1,27 @@
+//
+//  IPF.cpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 25/12/2021.
+//  Copyright © 2021 Thomas Harte. All rights reserved.
+//
+
+#include "IPF.hpp"
+
+using namespace Storage::Disk;
+
+
+IPF::IPF(const std::string &file_name) : file_(file_name) {
+}
+
+HeadPosition IPF::get_maximum_head_position() {
+	return HeadPosition(80); // TODO;
+}
+
+int IPF::get_head_count() {
+	return 2; // TODO;
+}
+
+std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Address address) {
+	return nullptr;
+}
diff --git a/Storage/Disk/DiskImage/Formats/IPF.hpp b/Storage/Disk/DiskImage/Formats/IPF.hpp
new file mode 100644
index 000000000..b7f08a52b
--- /dev/null
+++ b/Storage/Disk/DiskImage/Formats/IPF.hpp
@@ -0,0 +1,56 @@
+//
+//  IPF.hpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 25/12/2021.
+//  Copyright © 2021 Thomas Harte. All rights reserved.
+//
+
+#ifndef IPF_hpp
+#define IPF_hpp
+
+#include "../DiskImage.hpp"
+#include "../../../FileHolder.hpp"
+#include "../../../TargetPlatforms.hpp"
+
+#include <string>
+
+namespace Storage {
+namespace Disk {
+
+/*!
+	Provides a @c DiskImage containing an IPF.
+*/
+class IPF: public DiskImage, public TargetPlatform::TypeDistinguisher {
+	public:
+		/*!
+			Construct an @c IPF containing content from the file with name @c file_name.
+
+			@throws Storage::FileHolder::Error::CantOpen if this file can't be opened.
+			@throws Error::InvalidFormat if the file doesn't appear to contain an .HFE format image.
+			@throws Error::UnknownVersion if the file looks correct but is an unsupported version.
+		*/
+		IPF(const std::string &file_name);
+
+		// implemented to satisfy @c Disk
+		HeadPosition get_maximum_head_position() final;
+		int get_head_count() final;
+		std::shared_ptr<Track> get_track_at_position(Track::Address address) final;
+
+	private:
+		Storage::FileHolder file_;
+		uint16_t seek_track(Track::Address address);
+
+		int head_count_;
+		int track_count_;
+
+		TargetPlatform::Type target_platform_type() final {
+			return platform_type_;
+		}
+		TargetPlatform::Type platform_type_ = TargetPlatform::Amiga;
+};
+
+}
+}
+
+#endif /* IPF_hpp */

From c118dd8afe15bb9dd6005186b0064c82c139a829 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 25 Dec 2021 17:27:50 -0500
Subject: [PATCH 03/25] Adds just enough to list all the blocks in an IPF.

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index 44999490e..f5056073f 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -12,6 +12,28 @@ using namespace Storage::Disk;
 
 
 IPF::IPF(const std::string &file_name) : file_(file_name) {
+	while(true) {
+		const auto start_of_block = file_.tell();
+		const uint32_t type = file_.get32be();
+		uint32_t length = file_.get32be();						// Can't be const because of the dumb encoding of DATA blocks.
+		[[maybe_unused]] const uint32_t crc = file_.get32be();
+		if(file_.eof()) break;
+
+#define BLOCK(a, b, c, d) (a << 24) | (b << 16) | (c << 8) | d
+		switch(type) {
+			default:
+				printf("Ignoring %c%c%c%c, starting at %ld of length %d\n", (type >> 24), (type >> 16) & 0xff, (type >> 8) & 0xff, type & 0xff, start_of_block, length);
+			break;
+
+			case BLOCK('D', 'A', 'T', 'A'): {
+				length += file_.get32be();
+				printf("Handling DATA block at %ld of length %d\n", start_of_block, length);
+			} break;
+		}
+#undef BLOCK
+
+		file_.seek(start_of_block + length, SEEK_SET);
+	}
 }
 
 HeadPosition IPF::get_maximum_head_position() {

From e457ce66ea01d5968806f5370729cabca8ad9d35 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 25 Dec 2021 17:32:29 -0500
Subject: [PATCH 04/25] Adds sanity checks around CAPS block.

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index f5056073f..45220eb8e 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -19,12 +19,26 @@ IPF::IPF(const std::string &file_name) : file_(file_name) {
 		[[maybe_unused]] const uint32_t crc = file_.get32be();
 		if(file_.eof()) break;
 
-#define BLOCK(a, b, c, d) (a << 24) | (b << 16) | (c << 8) | d
+#define BLOCK(a, b, c, d) ((a << 24) | (b << 16) | (c << 8) | d)
+
+		// Sanity check: the first thing in a file should be the CAPS record.
+		if(!start_of_block && type != BLOCK('C', 'A', 'P', 'S')) {
+			throw Error::InvalidFormat;
+		}
+
 		switch(type) {
 			default:
 				printf("Ignoring %c%c%c%c, starting at %ld of length %d\n", (type >> 24), (type >> 16) & 0xff, (type >> 8) & 0xff, type & 0xff, start_of_block, length);
 			break;
 
+			case BLOCK('C', 'A', 'P', 'S'):
+				// Analogously to the sanity check above, if a CAPS block is anywhere other
+				// than first then something is amiss.
+				if(start_of_block) {
+					throw Error::InvalidFormat;
+				}
+			break;
+
 			case BLOCK('D', 'A', 'T', 'A'): {
 				length += file_.get32be();
 				printf("Handling DATA block at %ld of length %d\n", start_of_block, length);

From a6b326da4831ef6fac5e279e0cee497a3022ebfc Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 25 Dec 2021 18:17:13 -0500
Subject: [PATCH 05/25] Parse the INFO record.

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 50 ++++++++++++++++++++++++--
 Storage/Disk/DiskImage/Formats/IPF.hpp |  4 +--
 2 files changed, 50 insertions(+), 4 deletions(-)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index 45220eb8e..5720b44ab 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -39,6 +39,52 @@ IPF::IPF(const std::string &file_name) : file_(file_name) {
 				}
 			break;
 
+			case BLOCK('I', 'N', 'F', 'O'): {
+				// There are a lot of useful archival fields in the info chunk, which for emulation
+				// aren't that interesting.
+
+				// Make sure this is a floppy disk.
+				const uint32_t media_type = file_.get32be();
+				if(media_type != 1) {
+					throw Error::InvalidFormat;
+				}
+
+				// Skip: encoder type, revision, file key and revision, CRC of the original .ctr, and minimum track.
+				file_.seek(24, SEEK_CUR);
+				track_count_ = int(1 + file_.get32be());
+
+				// Skip: min side.
+				file_.seek(4, SEEK_CUR);
+				head_count_ = int(1 + file_.get32be());
+
+				// Skip: creation date, time.
+				file_.seek(8, SEEK_CUR);
+
+				platform_type_ = 0;
+				for(int c = 0; c < 4; c++) {
+					const uint8_t platform = file_.get8();
+					switch(platform) {
+						default: break;
+						case 1:	platform_type_ |= TargetPlatform::Amiga;		break;
+						case 2:	platform_type_ |= TargetPlatform::AtariST;		break;
+						/* Omitted: 3 -> IBM PC */
+						case 4:	platform_type_ |= TargetPlatform::AmstradCPC;	break;
+						case 5:	platform_type_ |= TargetPlatform::ZXSpectrum;	break;
+						/* Omitted: 6 -> Sam Coupé */
+						/* Omitted: 7 -> Archimedes */
+						/* Omitted: 8 -> C64 */
+						/* Omitted: 9 -> Atari 8-bit */
+					}
+				}
+
+				// If the file didn't declare anything, default to supporting everything.
+				if(!platform_type_) {
+					platform_type_ = ~0;
+				}
+
+				// Ignore: disk number, creator ID, reserved area.
+			} break;
+
 			case BLOCK('D', 'A', 'T', 'A'): {
 				length += file_.get32be();
 				printf("Handling DATA block at %ld of length %d\n", start_of_block, length);
@@ -51,11 +97,11 @@ IPF::IPF(const std::string &file_name) : file_(file_name) {
 }
 
 HeadPosition IPF::get_maximum_head_position() {
-	return HeadPosition(80); // TODO;
+	return HeadPosition(track_count_);
 }
 
 int IPF::get_head_count() {
-	return 2; // TODO;
+	return head_count_;
 }
 
 std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Address address) {
diff --git a/Storage/Disk/DiskImage/Formats/IPF.hpp b/Storage/Disk/DiskImage/Formats/IPF.hpp
index b7f08a52b..ad016f29a 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.hpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.hpp
@@ -45,9 +45,9 @@ class IPF: public DiskImage, public TargetPlatform::TypeDistinguisher {
 		int track_count_;
 
 		TargetPlatform::Type target_platform_type() final {
-			return platform_type_;
+			return TargetPlatform::Type(platform_type_);
 		}
-		TargetPlatform::Type platform_type_ = TargetPlatform::Amiga;
+		TargetPlatform::IntType platform_type_ = TargetPlatform::Amiga;
 };
 
 }

From 0433db037049e24f0e2add5f8fc07c04353ad278 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 25 Dec 2021 19:36:54 -0500
Subject: [PATCH 06/25] Eliminate macro.

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 20 +++++++++++++-------
 1 file changed, 13 insertions(+), 7 deletions(-)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index 5720b44ab..c97656be2 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -10,6 +10,15 @@
 
 using namespace Storage::Disk;
 
+namespace {
+
+constexpr uint32_t block(const char *src) {
+	static_assert(sizeof(int) >= sizeof(uint32_t));
+	return uint32_t((src[0] << 24) | (src[1] << 16) | (src[2] << 8) | src[3]);
+}
+
+}
+
 
 IPF::IPF(const std::string &file_name) : file_(file_name) {
 	while(true) {
@@ -19,10 +28,8 @@ IPF::IPF(const std::string &file_name) : file_(file_name) {
 		[[maybe_unused]] const uint32_t crc = file_.get32be();
 		if(file_.eof()) break;
 
-#define BLOCK(a, b, c, d) ((a << 24) | (b << 16) | (c << 8) | d)
-
 		// Sanity check: the first thing in a file should be the CAPS record.
-		if(!start_of_block && type != BLOCK('C', 'A', 'P', 'S')) {
+		if(!start_of_block && type != block("CAPS")) {
 			throw Error::InvalidFormat;
 		}
 
@@ -31,7 +38,7 @@ IPF::IPF(const std::string &file_name) : file_(file_name) {
 				printf("Ignoring %c%c%c%c, starting at %ld of length %d\n", (type >> 24), (type >> 16) & 0xff, (type >> 8) & 0xff, type & 0xff, start_of_block, length);
 			break;
 
-			case BLOCK('C', 'A', 'P', 'S'):
+			case block("CAPS"):
 				// Analogously to the sanity check above, if a CAPS block is anywhere other
 				// than first then something is amiss.
 				if(start_of_block) {
@@ -39,7 +46,7 @@ IPF::IPF(const std::string &file_name) : file_(file_name) {
 				}
 			break;
 
-			case BLOCK('I', 'N', 'F', 'O'): {
+			case block("INFO"): {
 				// There are a lot of useful archival fields in the info chunk, which for emulation
 				// aren't that interesting.
 
@@ -85,12 +92,11 @@ IPF::IPF(const std::string &file_name) : file_(file_name) {
 				// Ignore: disk number, creator ID, reserved area.
 			} break;
 
-			case BLOCK('D', 'A', 'T', 'A'): {
+			case block("DATA"): {
 				length += file_.get32be();
 				printf("Handling DATA block at %ld of length %d\n", start_of_block, length);
 			} break;
 		}
-#undef BLOCK
 
 		file_.seek(start_of_block + length, SEEK_SET);
 	}

From 28572d4392abe36082c243c6afcb8cefa76ddb17 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 26 Dec 2021 09:12:44 -0500
Subject: [PATCH 07/25] Enforce string-length requirement.

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index c97656be2..ca55cd1ae 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -12,9 +12,13 @@ using namespace Storage::Disk;
 
 namespace {
 
-constexpr uint32_t block(const char *src) {
-	static_assert(sizeof(int) >= sizeof(uint32_t));
-	return uint32_t((src[0] << 24) | (src[1] << 16) | (src[2] << 8) | src[3]);
+constexpr uint32_t block(const char (& src)[5]) {
+	return uint32_t(
+		(uint32_t(src[0]) << 24) |
+		(uint32_t(src[1]) << 16) |
+		(uint32_t(src[2]) << 8) |
+		uint32_t(src[3])
+	);
 }
 
 }
@@ -92,6 +96,9 @@ IPF::IPF(const std::string &file_name) : file_(file_name) {
 				// Ignore: disk number, creator ID, reserved area.
 			} break;
 
+			case block("IMGE"):
+			break;
+
 			case block("DATA"): {
 				length += file_.get32be();
 				printf("Handling DATA block at %ld of length %d\n", start_of_block, length);

From 9d3cf9c73cae11ec161b9f9bf76d9aaba9cf1804 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 26 Dec 2021 14:49:51 -0500
Subject: [PATCH 08/25] Collate descriptions of all tracks.

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 61 ++++++++++++++++++++++++--
 Storage/Disk/DiskImage/Formats/IPF.hpp | 25 +++++++++++
 2 files changed, 83 insertions(+), 3 deletions(-)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index ca55cd1ae..978f24b12 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -25,6 +25,10 @@ constexpr uint32_t block(const char (& src)[5]) {
 
 
 IPF::IPF(const std::string &file_name) : file_(file_name) {
+	std::map<uint32_t, Track::Address> tracks_by_data_key;
+
+	// For now, just build up a list of tracks that exist, noting the file position at which their data begins
+	// plus the other fields that'll be necessary to convert them into flux on demand later.
 	while(true) {
 		const auto start_of_block = file_.tell();
 		const uint32_t type = file_.get32be();
@@ -96,12 +100,63 @@ IPF::IPF(const std::string &file_name) : file_(file_name) {
 				// Ignore: disk number, creator ID, reserved area.
 			} break;
 
-			case block("IMGE"):
-			break;
+			case block("IMGE"): {
+				// Get track location.
+				const uint32_t track = file_.get32be();
+				const uint32_t side = file_.get32be();
+				const Track::Address address{int(side), HeadPosition(int(track))};
+
+				// Hence generate a TrackDescription.
+				auto pair = tracks_.emplace(address, TrackDescription());
+				TrackDescription &description = pair.first->second;
+
+				// Read those fields of interest...
+
+				// Bit density. I've no idea why the density can't just be given as a measurement.
+				description.density = TrackDescription::Density(file_.get32be());
+				if(description.density > TrackDescription::Density::Max) {
+					description.density = TrackDescription::Density::Unknown;
+				}
+
+
+				file_.seek(12, SEEK_CUR);	// Skipped: signal type, track bytes, start byte position.
+				description.start_bit_pos = file_.get32be();
+				description.data_bits = file_.get32be();
+				description.gap_bits = file_.get32be();
+
+				file_.seek(4, SEEK_CUR);	// Skipped: track bits, which is entirely redundant.
+				description.block_count = file_.get32be();
+
+				file_.seek(4, SEEK_CUR);	// Skipped: encoder process.
+				description.has_fuzzy_bits = file_.get32be() & 1;
+
+				// For some reason the authors decided to introduce another primary key,
+				// in addition to that which naturally exists of (track, side). So set up
+				// a mapping from the one to the other.
+				const uint32_t data_key = file_.get32be();
+				tracks_by_data_key.emplace(data_key, address);
+			} break;
 
 			case block("DATA"): {
 				length += file_.get32be();
-				printf("Handling DATA block at %ld of length %d\n", start_of_block, length);
+
+				file_.seek(8, SEEK_CUR);	// Skipped: bit size, CRC.
+
+				// Grab the data key and use that to establish the file starting
+				// position for this track.
+				//
+				// Assumed here: DATA records will come after corresponding IMGE records.
+				const uint32_t data_key = file_.get32be();
+				const auto pair = tracks_by_data_key.find(data_key);
+				if(pair == tracks_by_data_key.end()) {
+					break;
+				}
+
+				auto description = tracks_.find(pair->second);
+				if(description == tracks_.end()) {
+					break;
+				}
+				description->second.file_offset = file_.tell();
 			} break;
 		}
 
diff --git a/Storage/Disk/DiskImage/Formats/IPF.hpp b/Storage/Disk/DiskImage/Formats/IPF.hpp
index ad016f29a..993704546 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.hpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.hpp
@@ -14,6 +14,7 @@
 #include "../../../TargetPlatforms.hpp"
 
 #include <string>
+#include <map>
 
 namespace Storage {
 namespace Disk {
@@ -41,8 +42,32 @@ class IPF: public DiskImage, public TargetPlatform::TypeDistinguisher {
 		Storage::FileHolder file_;
 		uint16_t seek_track(Track::Address address);
 
+		struct TrackDescription {
+			long file_offset = 0;
+			enum class Density {
+				Unknown,
+				Noise,
+				Auto,
+				CopylockAmiga,
+				CopylockAmigaNew,
+				CopylockST,
+				SpeedlockAmiga,
+				OldSpeedlockAmiga,
+				AdamBrierleyAmiga,
+				AdamBrierleyDensityKeyAmiga,
+
+				Max = AdamBrierleyDensityKeyAmiga
+			} density = Density::Unknown;
+			uint32_t start_bit_pos = 0;
+			uint32_t data_bits = 0;
+			uint32_t gap_bits = 0;
+			uint32_t block_count;
+			bool has_fuzzy_bits = false;
+		};
+
 		int head_count_;
 		int track_count_;
+		std::map<Track::Address, TrackDescription> tracks_;
 
 		TargetPlatform::Type target_platform_type() final {
 			return TargetPlatform::Type(platform_type_);

From 9b6ccbcc957b6721899f4520cd14ac92fde59ff2 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Mon, 27 Dec 2021 18:12:44 -0500
Subject: [PATCH 09/25] Parses data and gap stream elements.

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 122 ++++++++++++++++++++++++-
 Storage/Disk/DiskImage/Formats/IPF.hpp |   1 +
 2 files changed, 121 insertions(+), 2 deletions(-)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index 978f24b12..e7a039700 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -21,6 +21,15 @@ constexpr uint32_t block(const char (& src)[5]) {
 	);
 }
 
+constexpr size_t block_size(Storage::FileHolder &file, uint8_t header) {
+	uint8_t size_width = header >> 5;
+	size_t length = 0;
+	while(size_width--) {
+		length = (length << 8) | file.get8();
+	}
+	return length;
+}
+
 }
 
 
@@ -64,8 +73,11 @@ IPF::IPF(const std::string &file_name) : file_(file_name) {
 					throw Error::InvalidFormat;
 				}
 
-				// Skip: encoder type, revision, file key and revision, CRC of the original .ctr, and minimum track.
-				file_.seek(24, SEEK_CUR);
+				// Determine whether this is a newer SPS-style file.
+				is_sps_format_ = file_.get32be() > 1;
+
+				// Skip: revision, file key and revision, CRC of the original .ctr, and minimum track.
+				file_.seek(20, SEEK_CUR);
 				track_count_ = int(1 + file_.get32be());
 
 				// Skip: min side.
@@ -173,5 +185,111 @@ int IPF::get_head_count() {
 }
 
 std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Address address) {
+	// Get the track description, if it exists, and check either that the file has contents for the track.
+	auto pair = tracks_.find(address);
+	if(pair == tracks_.end()) {
+		return nullptr;
+	}
+	const TrackDescription &description = pair->second;
+	if(!description.file_offset) {
+		return nullptr;
+	}
+
+	// Seek to track content.
+	file_.seek(description.file_offset, SEEK_SET);
+
+	// Read the block descriptions up front.
+	//
+	// This is less efficient than just seeking for each block in turn,
+	// but is a useful crutch to comprehension of the file format on a
+	// first run through.
+	struct BlockDescriptor {
+		uint32_t data_bits = 0;
+		uint32_t gap_bits = 0;
+		uint32_t gap_offset = 0;
+		bool is_mfm = false;
+		bool has_forward_gap = false;
+		bool has_backwards_gap = false;
+		bool data_unit_is_bits = false;
+		uint32_t default_gap_value = 0;
+		uint32_t data_offset = 0;
+	};
+	std::vector<BlockDescriptor> blocks;
+	blocks.reserve(description.block_count);
+	for(uint32_t c = 0; c < description.block_count; c++) {
+		auto &block = blocks.emplace_back();
+		block.data_bits = file_.get32be();
+		block.gap_bits = file_.get32be();
+		if(is_sps_format_) {
+			block.gap_offset = file_.get32be();
+			file_.seek(4, SEEK_CUR);	// Skip 'cell type' which appears to provide no content.
+		} else {
+			// Skip potlower-resolution copies of data_bits and gap_bits.
+			file_.seek(8, SEEK_CUR);
+		}
+		block.is_mfm = file_.get32be() == 1;
+
+		const uint32_t flags = file_.get32be();
+		block.has_forward_gap = flags & 1;
+		block.has_backwards_gap = flags & 2;
+		block.data_unit_is_bits = flags & 4;
+
+		block.default_gap_value = file_.get32be();
+		block.data_offset = file_.get32be();
+	}
+
+	// TODO: Append as necessary for each gap and data stream as per above.
+	for(auto &block: blocks) {
+		if(block.gap_offset) {
+			file_.seek(description.file_offset + block.gap_offset, SEEK_SET);
+			while(true) {
+				const uint8_t gap_header = file_.get8();
+				if(!gap_header) break;
+
+				// Decompose the header and read the length.
+				enum class Type {
+					None, GapLength, SampleLength
+				} type = Type(gap_header & 0x1f);
+				const size_t length = block_size(file_, gap_header);
+
+				// TODO: write the gap.
+				switch(type) {
+					case Type::GapLength:
+						printf("Unhandled gap length %zu\n", length);
+					break;
+
+					default:
+					case Type::SampleLength:
+						printf("Unhandled sampled gap length %zu\n", length);
+						file_.seek(long(length >> 3), SEEK_CUR);
+					break;
+				}
+			}
+		}
+
+		if(block.data_offset) {
+			file_.seek(description.file_offset + block.data_offset, SEEK_SET);
+			while(true) {
+				const uint8_t data_header = file_.get8();
+				if(!data_header) break;
+
+				// Decompose the header and read the length.
+				enum class Type {
+					None, Sync, Data, Gap, Raw, Fuzzy
+				} type = Type(data_header & 0x1f);
+				const size_t length = block_size(file_, data_header) * (block.data_unit_is_bits ? 1 : 8);
+
+				// TODO: write the data.
+				switch(type) {
+					default:
+					printf("Unhandled data type %d, length %zu\n", int(type), length);
+						file_.seek(long(length >> 3), SEEK_CUR);
+					break;
+				}
+			}
+		}
+		printf("\n");
+	}
+
 	return nullptr;
 }
diff --git a/Storage/Disk/DiskImage/Formats/IPF.hpp b/Storage/Disk/DiskImage/Formats/IPF.hpp
index 993704546..306aa693d 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.hpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.hpp
@@ -68,6 +68,7 @@ class IPF: public DiskImage, public TargetPlatform::TypeDistinguisher {
 		int head_count_;
 		int track_count_;
 		std::map<Track::Address, TrackDescription> tracks_;
+		bool is_sps_format_ = false;
 
 		TargetPlatform::Type target_platform_type() final {
 			return TargetPlatform::Type(platform_type_);

From dc994f001dcaa9a7a3a687c1f8c05201253690ac Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Mon, 27 Dec 2021 18:55:11 -0500
Subject: [PATCH 10/25] Mention units.

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index e7a039700..47cb56246 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -255,12 +255,12 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 				// TODO: write the gap.
 				switch(type) {
 					case Type::GapLength:
-						printf("Unhandled gap length %zu\n", length);
+						printf("Unhandled gap length %zu bytes\n", length);
 					break;
 
 					default:
 					case Type::SampleLength:
-						printf("Unhandled sampled gap length %zu\n", length);
+						printf("Unhandled sampled gap length %zu bytes\n", length);
 						file_.seek(long(length >> 3), SEEK_CUR);
 					break;
 				}
@@ -282,7 +282,7 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 				// TODO: write the data.
 				switch(type) {
 					default:
-					printf("Unhandled data type %d, length %zu\n", int(type), length);
+						printf("Unhandled data type %d, length %zu bits\n", int(type), length);
 						file_.seek(long(length >> 3), SEEK_CUR);
 					break;
 				}

From 4f3c75477118f095fc691b9b2fea331cf7f0891d Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Mon, 27 Dec 2021 19:15:46 -0500
Subject: [PATCH 11/25] Adds exposition.

---
 Storage/Disk/DiskImage/Formats/IPF.hpp | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.hpp b/Storage/Disk/DiskImage/Formats/IPF.hpp
index 306aa693d..20352c99c 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.hpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.hpp
@@ -20,7 +20,10 @@ namespace Storage {
 namespace Disk {
 
 /*!
-	Provides a @c DiskImage containing an IPF.
+	Provides a @c DiskImage containing an IPF, which is a mixed stream of raw flux windows and
+	unencoded MFM sections along with gap records that can be used to record write splices, all
+	of which is variably clocked (albeit not at flux transition resolution; as a result IPF files tend to be
+	close in size to more primitive formats).
 */
 class IPF: public DiskImage, public TargetPlatform::TypeDistinguisher {
 	public:

From 350c98ab4d6fb6d6fec2065cdc45b25e20d71bb8 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Wed, 29 Dec 2021 18:15:37 -0500
Subject: [PATCH 12/25] Add those densities I've yet discovered the rules for.

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 61 +++++++++++++++++++++++++-
 Storage/Disk/DiskImage/Formats/IPF.hpp |  2 +
 2 files changed, 61 insertions(+), 2 deletions(-)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index 47cb56246..6f21e53c6 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -32,7 +32,6 @@ constexpr size_t block_size(Storage::FileHolder &file, uint8_t header) {
 
 }
 
-
 IPF::IPF(const std::string &file_name) : file_(file_name) {
 	std::map<uint32_t, Track::Address> tracks_by_data_key;
 
@@ -130,7 +129,6 @@ IPF::IPF(const std::string &file_name) : file_(file_name) {
 					description.density = TrackDescription::Density::Unknown;
 				}
 
-
 				file_.seek(12, SEEK_CUR);	// Skipped: signal type, track bytes, start byte position.
 				description.start_bit_pos = file_.get32be();
 				description.data_bits = file_.get32be();
@@ -293,3 +291,62 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 
 	return nullptr;
 }
+
+/// @returns A vector of the length of a bit in each block for a count of @c blocks in an area of data density @c density.
+///
+/// @discussion At least to me, this is the least well-designed part] of the IPF specification; rather than just dictating cell
+/// densities (or, equivalently, lengths) in the file, densities are named according to their protection scheme and the decoder
+/// is required to know all named protection schemes. Which makes IPF unable to handle arbitrary disks (or, indeed, disks
+/// with multiple protection schemes on a single track).
+std::vector<Storage::Time> IPF::bit_lengths(TrackDescription::Density density, size_t blocks) {
+	std::vector<Storage::Time> result;
+	result.reserve(size_t(blocks));
+
+	// Establish the default density of 2 µs.
+	for(size_t c = 0; c < blocks; c++) {
+		result.push_back(Storage::Time(1, 500'000));	// i.e. default to 2µs.
+	}
+
+	switch(density) {
+		default:
+		break;
+
+		case TrackDescription::Density::CopylockAmiga:
+			if(blocks > 4) result[4] = Storage::Time(189, 100'000'000);		// 1.89µs
+			if(blocks > 5) result[5] = Storage::Time(199, 100'000'000);		// 1.99µs
+			if(blocks > 6) result[6] = Storage::Time(209, 100'000'000);		// 2.09µs
+		break;
+
+		case TrackDescription::Density::CopylockAmigaNew:
+			if(blocks > 0) result[0] = Storage::Time(189, 100'000'000);		// 1.89µs
+			if(blocks > 1) result[1] = Storage::Time(199, 100'000'000);		// 1.99µs
+			if(blocks > 2) result[2] = Storage::Time(209, 100'000'000);		// 2.09µs
+		break;
+
+		case TrackDescription::Density::CopylockST:
+			if(blocks > 5) result[5] = Storage::Time(21, 10'000'000);		// 2.1µs
+		break;
+
+		case TrackDescription::Density::SpeedlockAmiga:
+			if(blocks > 1) result[1] = Storage::Time(11, 5'000'000);		// 2.2µs
+			if(blocks > 2) result[2] = Storage::Time(9, 5'000'000);			// 1.8µs
+		break;
+
+		case TrackDescription::Density::OldSpeedlockAmiga:
+			if(blocks > 1) result[1] = Storage::Time(21, 10'000'000);		// 2.1µs
+		break;
+
+		case TrackDescription::Density::AdamBrierleyAmiga:
+			if(blocks > 1) result[1] = Storage::Time(11, 5'000'000);		// 2.2µs
+			if(blocks > 2) result[2] = Storage::Time(21, 10'000'000);		// 2.1µs
+
+			if(blocks > 4) result[3] = Storage::Time(19, 10'000'000);		// 1.9µs
+			if(blocks > 5) result[5] = Storage::Time(9, 5'000'000);			// 1.8µs
+			if(blocks > 6) result[6] = Storage::Time(17, 10'000'000);		// 1.7µs
+		break;
+
+		// TODO: AdamBrierleyDensityKeyAmiga.
+	}
+
+	return result;
+}
diff --git a/Storage/Disk/DiskImage/Formats/IPF.hpp b/Storage/Disk/DiskImage/Formats/IPF.hpp
index 20352c99c..d8bd5d4d6 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.hpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.hpp
@@ -68,6 +68,8 @@ class IPF: public DiskImage, public TargetPlatform::TypeDistinguisher {
 			bool has_fuzzy_bits = false;
 		};
 
+		std::vector<Time> bit_lengths(TrackDescription::Density, size_t blocks);
+
 		int head_count_;
 		int track_count_;
 		std::map<Track::Address, TrackDescription> tracks_;

From d3189acaa63caf048025794aa3b0fb8f87faf6f6 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 1 Jan 2022 17:14:52 -0500
Subject: [PATCH 13/25] Add a constexpr route that explicitly calculates the
 simplest possible form.

---
 Storage/Storage.hpp | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/Storage/Storage.hpp b/Storage/Storage.hpp
index f096e63bb..90db2b5cd 100644
--- a/Storage/Storage.hpp
+++ b/Storage/Storage.hpp
@@ -33,6 +33,10 @@ struct Time {
 	Time(float value) {
 		install_float(value);
 	}
+	static constexpr Time simplified(unsigned int _length, unsigned int _clock_rate) {
+		const auto gcd = std::gcd(_length, _clock_rate);
+		return Time(_length / gcd, _clock_rate / gcd);
+	}
 
 	/*!
 		Reduces this @c Time to its simplest form; eliminates all common factors from @c length

From 38dd3c5c609ac3b3dbe78b7d0c58f1a40fab8c81 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 1 Jan 2022 17:15:12 -0500
Subject: [PATCH 14/25] On second thoughts, no need to use a vector here.

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 55 ++++++++++++++------------
 Storage/Disk/DiskImage/Formats/IPF.hpp |  2 +-
 2 files changed, 30 insertions(+), 27 deletions(-)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index 6f21e53c6..d608cb167 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -292,61 +292,64 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 	return nullptr;
 }
 
-/// @returns A vector of the length of a bit in each block for a count of @c blocks in an area of data density @c density.
+/// @returns The correct bit length for @c block on a track of @c density.
 ///
 /// @discussion At least to me, this is the least well-designed part] of the IPF specification; rather than just dictating cell
 /// densities (or, equivalently, lengths) in the file, densities are named according to their protection scheme and the decoder
 /// is required to know all named protection schemes. Which makes IPF unable to handle arbitrary disks (or, indeed, disks
 /// with multiple protection schemes on a single track).
-std::vector<Storage::Time> IPF::bit_lengths(TrackDescription::Density density, size_t blocks) {
-	std::vector<Storage::Time> result;
-	result.reserve(size_t(blocks));
-
-	// Establish the default density of 2 µs.
-	for(size_t c = 0; c < blocks; c++) {
-		result.push_back(Storage::Time(1, 500'000));	// i.e. default to 2µs.
-	}
+Storage::Time IPF::bit_length(TrackDescription::Density density, int block) {
+	constexpr unsigned int us = 100'000'000;
+	static constexpr auto us170 = Storage::Time::simplified(170, us);
+	static constexpr auto us180 = Storage::Time::simplified(180, us);
+	static constexpr auto us189 = Storage::Time::simplified(189, us);
+	static constexpr auto us190 = Storage::Time::simplified(190, us);
+	static constexpr auto us199 = Storage::Time::simplified(199, us);
+	static constexpr auto us200 = Storage::Time::simplified(200, us);
+	static constexpr auto us209 = Storage::Time::simplified(209, us);
+	static constexpr auto us210 = Storage::Time::simplified(210, us);
+	static constexpr auto us220 = Storage::Time::simplified(220, us);
 
 	switch(density) {
 		default:
 		break;
 
 		case TrackDescription::Density::CopylockAmiga:
-			if(blocks > 4) result[4] = Storage::Time(189, 100'000'000);		// 1.89µs
-			if(blocks > 5) result[5] = Storage::Time(199, 100'000'000);		// 1.99µs
-			if(blocks > 6) result[6] = Storage::Time(209, 100'000'000);		// 2.09µs
+			if(block == 4) return us189;
+			if(block == 5) return us199;
+			if(block == 6) return us209;
 		break;
 
 		case TrackDescription::Density::CopylockAmigaNew:
-			if(blocks > 0) result[0] = Storage::Time(189, 100'000'000);		// 1.89µs
-			if(blocks > 1) result[1] = Storage::Time(199, 100'000'000);		// 1.99µs
-			if(blocks > 2) result[2] = Storage::Time(209, 100'000'000);		// 2.09µs
+			if(block == 0) return us189;
+			if(block == 1) return us199;
+			if(block == 2) return us209;
 		break;
 
 		case TrackDescription::Density::CopylockST:
-			if(blocks > 5) result[5] = Storage::Time(21, 10'000'000);		// 2.1µs
+			if(block == 5) return us210;
 		break;
 
 		case TrackDescription::Density::SpeedlockAmiga:
-			if(blocks > 1) result[1] = Storage::Time(11, 5'000'000);		// 2.2µs
-			if(blocks > 2) result[2] = Storage::Time(9, 5'000'000);			// 1.8µs
+			if(block == 1) return us220;
+			if(block == 2) return us180;
 		break;
 
 		case TrackDescription::Density::OldSpeedlockAmiga:
-			if(blocks > 1) result[1] = Storage::Time(21, 10'000'000);		// 2.1µs
+			if(block == 1) return us210;
 		break;
 
 		case TrackDescription::Density::AdamBrierleyAmiga:
-			if(blocks > 1) result[1] = Storage::Time(11, 5'000'000);		// 2.2µs
-			if(blocks > 2) result[2] = Storage::Time(21, 10'000'000);		// 2.1µs
-
-			if(blocks > 4) result[3] = Storage::Time(19, 10'000'000);		// 1.9µs
-			if(blocks > 5) result[5] = Storage::Time(9, 5'000'000);			// 1.8µs
-			if(blocks > 6) result[6] = Storage::Time(17, 10'000'000);		// 1.7µs
+			if(block == 1) return us220;
+			if(block == 2) return us210;
+			if(block == 3) return us200;
+			if(block == 4) return us190;
+			if(block == 5) return us180;
+			if(block == 6) return us170;
 		break;
 
 		// TODO: AdamBrierleyDensityKeyAmiga.
 	}
 
-	return result;
+	return us200;	// i.e. default to 2µs.
 }
diff --git a/Storage/Disk/DiskImage/Formats/IPF.hpp b/Storage/Disk/DiskImage/Formats/IPF.hpp
index d8bd5d4d6..1ce1e74c7 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.hpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.hpp
@@ -68,7 +68,7 @@ class IPF: public DiskImage, public TargetPlatform::TypeDistinguisher {
 			bool has_fuzzy_bits = false;
 		};
 
-		std::vector<Time> bit_lengths(TrackDescription::Density, size_t blocks);
+		Time bit_length(TrackDescription::Density, int block);
 
 		int head_count_;
 		int track_count_;

From ed1b0b90f7c18b21bb1b12e791937c679450b967 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 1 Jan 2022 18:36:44 -0500
Subject: [PATCH 15/25] Makes a first attempt at encoding data.

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 59 ++++++++++++++++++++++++--
 1 file changed, 56 insertions(+), 3 deletions(-)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index d608cb167..1d2e82997 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -8,6 +8,9 @@
 
 #include "IPF.hpp"
 
+#include "../../Track/PCMTrack.hpp"
+#include "../../Encodings/MFM/Encoder.hpp"
+
 using namespace Storage::Disk;
 
 namespace {
@@ -236,8 +239,11 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 		block.data_offset = file_.get32be();
 	}
 
-	// TODO: Append as necessary for each gap and data stream as per above.
+	std::vector<Storage::Disk::PCMSegment> segments;
+	int block_count = 0;
 	for(auto &block: blocks) {
+		const auto length_of_a_bit = bit_length(description.density, block_count);
+
 		if(block.gap_offset) {
 			file_.seek(description.file_offset + block.gap_offset, SEEK_SET);
 			while(true) {
@@ -279,6 +285,52 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 
 				// TODO: write the data.
 				switch(type) {
+					case Type::Gap: {
+						auto &segment = segments.emplace_back();
+						segment.length_of_a_bit = length_of_a_bit;
+						segment.data.reserve(size_t(length + 31) & size_t(~31));
+						auto encoder = Storage::Encodings::MFM::GetMFMEncoder(segment.data);
+						while(segment.data.size() < length) {
+							encoder->add_byte(uint8_t(block.default_gap_value >> 24));
+							encoder->add_byte(uint8_t(block.default_gap_value >> 16));
+							encoder->add_byte(uint8_t(block.default_gap_value >> 8));
+							encoder->add_byte(uint8_t(block.default_gap_value >> 0));
+						}
+						segment.data.resize(length);
+					} break;
+
+					case Type::Data: {
+						auto &segment = segments.emplace_back();
+						segment.length_of_a_bit = length_of_a_bit;
+						segment.data.reserve(size_t(length + 7) & size_t(~7));
+						auto encoder = Storage::Encodings::MFM::GetMFMEncoder(segment.data);
+						while(segment.data.size() < length) {
+							encoder->add_byte(file_.get8());
+						}
+						segment.data.resize(length);
+					} break;
+
+					case Type::Sync:
+					case Type::Raw: {
+						auto &segment = segments.emplace_back();
+						segment.length_of_a_bit = length_of_a_bit;
+						segment.data.reserve(length);
+
+						for(size_t bit = 0; bit < length; bit += 8) {
+							const uint8_t next = file_.get8();
+							segment.data.push_back(next & 0x80);
+							segment.data.push_back(next & 0x40);
+							segment.data.push_back(next & 0x20);
+							segment.data.push_back(next & 0x10);
+							segment.data.push_back(next & 0x08);
+							segment.data.push_back(next & 0x04);
+							segment.data.push_back(next & 0x02);
+							segment.data.push_back(next & 0x01);
+						}
+
+						segment.data.resize(length);
+					} break;
+
 					default:
 						printf("Unhandled data type %d, length %zu bits\n", int(type), length);
 						file_.seek(long(length >> 3), SEEK_CUR);
@@ -286,10 +338,11 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 				}
 			}
 		}
-		printf("\n");
+
+		++block_count;
 	}
 
-	return nullptr;
+	return std::make_shared<Storage::Disk::PCMTrack>(segments);
 }
 
 /// @returns The correct bit length for @c block on a track of @c density.

From d031381e70138dd73cc1e6f07b717ea0591c1583 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 1 Jan 2022 18:47:07 -0500
Subject: [PATCH 16/25] Gaps provide content, and data chunk lengths seem to be
 in terms of unencoded bytes.

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 42 +++++++++++++++++---------
 1 file changed, 27 insertions(+), 15 deletions(-)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index 1d2e82997..270bb234e 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -282,10 +282,13 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 					None, Sync, Data, Gap, Raw, Fuzzy
 				} type = Type(data_header & 0x1f);
 				const size_t length = block_size(file_, data_header) * (block.data_unit_is_bits ? 1 : 8);
+#ifndef NDEBUG
+				const auto next_chunk = file_.tell() + long(length >> 3);
+#endif
 
 				// TODO: write the data.
 				switch(type) {
-					case Type::Gap: {
+					/*case Type::Gap: {
 						auto &segment = segments.emplace_back();
 						segment.length_of_a_bit = length_of_a_bit;
 						segment.data.reserve(size_t(length + 31) & size_t(~31));
@@ -297,24 +300,32 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 							encoder->add_byte(uint8_t(block.default_gap_value >> 0));
 						}
 						segment.data.resize(length);
-					} break;
+					} break;*/
 
 					case Type::Data: {
+						printf("Handling data type %d, length %zu bits\n", int(type), length);
+						auto &segment = segments.emplace_back();
+						segment.length_of_a_bit = length_of_a_bit;
+
+						// Length appears to be in pre-encoded bits; double that to get encoded bits.
+						const auto byte_length = (length + 7) >> 3;
+						segment.data.reserve(byte_length * 2);
+
+						auto encoder = Storage::Encodings::MFM::GetMFMEncoder(segment.data);
+						for(size_t c = 0; c < (length >> 3); c++) {
+							encoder->add_byte(file_.get8());
+						}
+
+						segment.data.resize(length * 2);
+					} break;
+
+					case Type::Gap:
+					case Type::Sync:
+					case Type::Raw: {
+						printf("Handling data type %d, length %zu bits\n", int(type), length);
 						auto &segment = segments.emplace_back();
 						segment.length_of_a_bit = length_of_a_bit;
 						segment.data.reserve(size_t(length + 7) & size_t(~7));
-						auto encoder = Storage::Encodings::MFM::GetMFMEncoder(segment.data);
-						while(segment.data.size() < length) {
-							encoder->add_byte(file_.get8());
-						}
-						segment.data.resize(length);
-					} break;
-
-					case Type::Sync:
-					case Type::Raw: {
-						auto &segment = segments.emplace_back();
-						segment.length_of_a_bit = length_of_a_bit;
-						segment.data.reserve(length);
 
 						for(size_t bit = 0; bit < length; bit += 8) {
 							const uint8_t next = file_.get8();
@@ -333,9 +344,10 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 
 					default:
 						printf("Unhandled data type %d, length %zu bits\n", int(type), length);
-						file_.seek(long(length >> 3), SEEK_CUR);
 					break;
 				}
+
+				assert(file_.tell() == next_chunk);
 			}
 		}
 

From dc920a04f65b1c43ad676378fcd864dc03922f1d Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 1 Jan 2022 19:03:07 -0500
Subject: [PATCH 17/25] Add missing #include.

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index 270bb234e..d14d43306 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -11,6 +11,8 @@
 #include "../../Track/PCMTrack.hpp"
 #include "../../Encodings/MFM/Encoder.hpp"
 
+#include <cassert>
+
 using namespace Storage::Disk;
 
 namespace {

From 58d10943ed5c00198476ba807a5d8bdbde9ba400 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 1 Jan 2022 19:08:44 -0500
Subject: [PATCH 18/25] Add asserts to validate my reserve sizes.

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index d14d43306..3de324e29 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -311,13 +311,14 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 
 						// Length appears to be in pre-encoded bits; double that to get encoded bits.
 						const auto byte_length = (length + 7) >> 3;
-						segment.data.reserve(byte_length * 2);
+						segment.data.reserve(byte_length * 16);
 
 						auto encoder = Storage::Encodings::MFM::GetMFMEncoder(segment.data);
 						for(size_t c = 0; c < (length >> 3); c++) {
 							encoder->add_byte(file_.get8());
 						}
 
+						assert(segment.data.size() <= (byte_length * 16));
 						segment.data.resize(length * 2);
 					} break;
 
@@ -327,7 +328,9 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 						printf("Handling data type %d, length %zu bits\n", int(type), length);
 						auto &segment = segments.emplace_back();
 						segment.length_of_a_bit = length_of_a_bit;
-						segment.data.reserve(size_t(length + 7) & size_t(~7));
+
+						const auto bit_length = size_t(length + 7) & size_t(~7);
+						segment.data.reserve(bit_length);
 
 						for(size_t bit = 0; bit < length; bit += 8) {
 							const uint8_t next = file_.get8();
@@ -341,6 +344,7 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 							segment.data.push_back(next & 0x01);
 						}
 
+						assert(segment.data.size() <= bit_length);
 						segment.data.resize(length);
 					} break;
 

From 3e0b7d71d473f04451bf9d4936c08ea1738b373d Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sat, 1 Jan 2022 19:09:19 -0500
Subject: [PATCH 19/25] Properly handle partial bytes.

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index 3de324e29..063089cc8 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -314,7 +314,7 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 						segment.data.reserve(byte_length * 16);
 
 						auto encoder = Storage::Encodings::MFM::GetMFMEncoder(segment.data);
-						for(size_t c = 0; c < (length >> 3); c++) {
+						for(size_t c = 0; c < length; c += 8) {
 							encoder->add_byte(file_.get8());
 						}
 

From f37179d9f2c91b9e71895cbff8d711bea84d2631 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 2 Jan 2022 15:39:26 -0500
Subject: [PATCH 20/25] Gaps appear to contain pre-MFM data (?)

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index 063089cc8..ad7024207 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -304,6 +304,7 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 						segment.data.resize(length);
 					} break;*/
 
+					case Type::Gap:
 					case Type::Data: {
 						printf("Handling data type %d, length %zu bits\n", int(type), length);
 						auto &segment = segments.emplace_back();
@@ -322,7 +323,6 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 						segment.data.resize(length * 2);
 					} break;
 
-					case Type::Gap:
 					case Type::Sync:
 					case Type::Raw: {
 						printf("Handling data type %d, length %zu bits\n", int(type), length);

From 18b6f17e86da1527c19de0b88988b0b2cffdf7dd Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Thu, 6 Jan 2022 17:24:31 -0500
Subject: [PATCH 21/25] With some refactoring makes some minor steps towards
 supporting gaps.

---
 Storage/Disk/DiskImage/Formats/IPF.cpp | 131 ++++++++++++++-----------
 Storage/Disk/DiskImage/Formats/IPF.hpp |   8 +-
 2 files changed, 77 insertions(+), 62 deletions(-)

diff --git a/Storage/Disk/DiskImage/Formats/IPF.cpp b/Storage/Disk/DiskImage/Formats/IPF.cpp
index ad7024207..a610564d0 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.cpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.cpp
@@ -8,7 +8,6 @@
 
 #include "IPF.hpp"
 
-#include "../../Track/PCMTrack.hpp"
 #include "../../Encodings/MFM/Encoder.hpp"
 
 #include <cassert>
@@ -258,19 +257,22 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 				} type = Type(gap_header & 0x1f);
 				const size_t length = block_size(file_, gap_header);
 
-				// TODO: write the gap.
 				switch(type) {
 					case Type::GapLength:
-						printf("Unhandled gap length %zu bytes\n", length);
+						printf("Adding gap length %zu bits\n", length);
+						add_gap(segments, length_of_a_bit, length, block.default_gap_value);
 					break;
 
 					default:
 					case Type::SampleLength:
-						printf("Unhandled sampled gap length %zu bytes\n", length);
-						file_.seek(long(length >> 3), SEEK_CUR);
+						printf("Adding sampled gap length %zu bits\n", length);
+						add_raw_data(segments, length_of_a_bit, length);
+//						file_.seek(long(length >> 3), SEEK_CUR);
 					break;
 				}
 			}
+		} else if(block.gap_bits) {
+			add_gap(segments, length_of_a_bit, block.gap_bits, block.default_gap_value);
 		}
 
 		if(block.data_offset) {
@@ -288,65 +290,16 @@ std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Addres
 				const auto next_chunk = file_.tell() + long(length >> 3);
 #endif
 
-				// TODO: write the data.
 				switch(type) {
-					/*case Type::Gap: {
-						auto &segment = segments.emplace_back();
-						segment.length_of_a_bit = length_of_a_bit;
-						segment.data.reserve(size_t(length + 31) & size_t(~31));
-						auto encoder = Storage::Encodings::MFM::GetMFMEncoder(segment.data);
-						while(segment.data.size() < length) {
-							encoder->add_byte(uint8_t(block.default_gap_value >> 24));
-							encoder->add_byte(uint8_t(block.default_gap_value >> 16));
-							encoder->add_byte(uint8_t(block.default_gap_value >> 8));
-							encoder->add_byte(uint8_t(block.default_gap_value >> 0));
-						}
-						segment.data.resize(length);
-					} break;*/
-
 					case Type::Gap:
-					case Type::Data: {
-						printf("Handling data type %d, length %zu bits\n", int(type), length);
-						auto &segment = segments.emplace_back();
-						segment.length_of_a_bit = length_of_a_bit;
-
-						// Length appears to be in pre-encoded bits; double that to get encoded bits.
-						const auto byte_length = (length + 7) >> 3;
-						segment.data.reserve(byte_length * 16);
-
-						auto encoder = Storage::Encodings::MFM::GetMFMEncoder(segment.data);
-						for(size_t c = 0; c < length; c += 8) {
-							encoder->add_byte(file_.get8());
-						}
-
-						assert(segment.data.size() <= (byte_length * 16));
-						segment.data.resize(length * 2);
-					} break;
+					case Type::Data:
+						add_unencoded_data(segments, length_of_a_bit, length);
+					break;
 
 					case Type::Sync:
-					case Type::Raw: {
-						printf("Handling data type %d, length %zu bits\n", int(type), length);
-						auto &segment = segments.emplace_back();
-						segment.length_of_a_bit = length_of_a_bit;
-
-						const auto bit_length = size_t(length + 7) & size_t(~7);
-						segment.data.reserve(bit_length);
-
-						for(size_t bit = 0; bit < length; bit += 8) {
-							const uint8_t next = file_.get8();
-							segment.data.push_back(next & 0x80);
-							segment.data.push_back(next & 0x40);
-							segment.data.push_back(next & 0x20);
-							segment.data.push_back(next & 0x10);
-							segment.data.push_back(next & 0x08);
-							segment.data.push_back(next & 0x04);
-							segment.data.push_back(next & 0x02);
-							segment.data.push_back(next & 0x01);
-						}
-
-						assert(segment.data.size() <= bit_length);
-						segment.data.resize(length);
-					} break;
+					case Type::Raw:
+						add_raw_data(segments, length_of_a_bit, length);
+					break;
 
 					default:
 						printf("Unhandled data type %d, length %zu bits\n", int(type), length);
@@ -424,3 +377,61 @@ Storage::Time IPF::bit_length(TrackDescription::Density density, int block) {
 
 	return us200;	// i.e. default to 2µs.
 }
+
+void IPF::add_gap(std::vector<Storage::Disk::PCMSegment> &track, Time bit_length, size_t num_bits, uint32_t value) {
+	auto &segment = track.emplace_back();
+	segment.length_of_a_bit = bit_length;
+
+	// Empirically, I think gaps require MFM encoding.
+	const auto byte_length = (num_bits + 7) >> 3;
+	segment.data.reserve(byte_length * 16);
+
+	auto encoder = Storage::Encodings::MFM::GetMFMEncoder(segment.data);
+	while(segment.data.size() < num_bits) {
+		encoder->add_byte(uint8_t(value >> 24));
+		value = (value << 8) | (value >> 24);
+	}
+
+	assert(segment.data.size() <= (byte_length * 16));
+	segment.data.resize(num_bits);
+}
+
+void IPF::add_unencoded_data(std::vector<Storage::Disk::PCMSegment> &track, Time bit_length, size_t num_bits) {
+	auto &segment = track.emplace_back();
+	segment.length_of_a_bit = bit_length;
+
+	// Length appears to be in pre-encoded bits; double that to get encoded bits.
+	const auto byte_length = (num_bits + 7) >> 3;
+	segment.data.reserve(num_bits * 16);
+
+	auto encoder = Storage::Encodings::MFM::GetMFMEncoder(segment.data);
+	for(size_t c = 0; c < num_bits; c += 8) {
+		encoder->add_byte(file_.get8());
+	}
+
+	assert(segment.data.size() <= (byte_length * 16));
+	segment.data.resize(num_bits * 2);
+}
+
+void IPF::add_raw_data(std::vector<Storage::Disk::PCMSegment> &track, Time bit_length, size_t num_bits) {
+	auto &segment = track.emplace_back();
+	segment.length_of_a_bit = bit_length;
+
+	const auto num_bits_ceiling = size_t(num_bits + 7) & size_t(~7);
+	segment.data.reserve(num_bits_ceiling);
+
+	for(size_t bit = 0; bit < num_bits; bit += 8) {
+		const uint8_t next = file_.get8();
+		segment.data.push_back(next & 0x80);
+		segment.data.push_back(next & 0x40);
+		segment.data.push_back(next & 0x20);
+		segment.data.push_back(next & 0x10);
+		segment.data.push_back(next & 0x08);
+		segment.data.push_back(next & 0x04);
+		segment.data.push_back(next & 0x02);
+		segment.data.push_back(next & 0x01);
+	}
+
+	assert(segment.data.size() <= num_bits_ceiling);
+	segment.data.resize(num_bits);
+}
diff --git a/Storage/Disk/DiskImage/Formats/IPF.hpp b/Storage/Disk/DiskImage/Formats/IPF.hpp
index 1ce1e74c7..fa29aafa8 100644
--- a/Storage/Disk/DiskImage/Formats/IPF.hpp
+++ b/Storage/Disk/DiskImage/Formats/IPF.hpp
@@ -10,6 +10,7 @@
 #define IPF_hpp
 
 #include "../DiskImage.hpp"
+#include "../../Track/PCMTrack.hpp"
 #include "../../../FileHolder.hpp"
 #include "../../../TargetPlatforms.hpp"
 
@@ -68,8 +69,6 @@ class IPF: public DiskImage, public TargetPlatform::TypeDistinguisher {
 			bool has_fuzzy_bits = false;
 		};
 
-		Time bit_length(TrackDescription::Density, int block);
-
 		int head_count_;
 		int track_count_;
 		std::map<Track::Address, TrackDescription> tracks_;
@@ -79,6 +78,11 @@ class IPF: public DiskImage, public TargetPlatform::TypeDistinguisher {
 			return TargetPlatform::Type(platform_type_);
 		}
 		TargetPlatform::IntType platform_type_ = TargetPlatform::Amiga;
+
+		Time bit_length(TrackDescription::Density, int block);
+		void add_gap(std::vector<Storage::Disk::PCMSegment> &, Time bit_length, size_t num_bits, uint32_t value);
+		void add_unencoded_data(std::vector<Storage::Disk::PCMSegment> &, Time bit_length, size_t num_bits);
+		void add_raw_data(std::vector<Storage::Disk::PCMSegment> &, Time bit_length, size_t num_bits);
 };
 
 }

From e994910ff67cfa1965ae2274694b918be5213650 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Thu, 2 Jun 2022 16:46:41 -0400
Subject: [PATCH 22/25] Switch to `unique_ptr`.

---
 Concurrency/AsyncTaskQueue.cpp | 7 ++++---
 Concurrency/AsyncTaskQueue.hpp | 4 +---
 2 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/Concurrency/AsyncTaskQueue.cpp b/Concurrency/AsyncTaskQueue.cpp
index 798683c39..66b0fdaa3 100644
--- a/Concurrency/AsyncTaskQueue.cpp
+++ b/Concurrency/AsyncTaskQueue.cpp
@@ -88,16 +88,17 @@ DeferringAsyncTaskQueue::~DeferringAsyncTaskQueue() {
 
 void DeferringAsyncTaskQueue::defer(std::function<void(void)> function) {
 	if(!deferred_tasks_) {
-		deferred_tasks_ = std::make_shared<std::list<std::function<void(void)>>>();
+		deferred_tasks_ = std::make_unique<std::list<std::function<void(void)>>>();
 	}
 	deferred_tasks_->push_back(function);
 }
 
 void DeferringAsyncTaskQueue::perform() {
 	if(!deferred_tasks_) return;
-	std::shared_ptr<std::list<std::function<void(void)>>> deferred_tasks = deferred_tasks_;
+	auto deferred_tasks_raw = deferred_tasks_.release();
 	deferred_tasks_.reset();
-	enqueue([deferred_tasks] {
+	enqueue([deferred_tasks_raw] {
+		std::unique_ptr<std::list<std::function<void(void)>>> deferred_tasks(deferred_tasks_raw);
 		for(const auto &function : *deferred_tasks) {
 			function();
 		}
diff --git a/Concurrency/AsyncTaskQueue.hpp b/Concurrency/AsyncTaskQueue.hpp
index da5d052c8..2962e329c 100644
--- a/Concurrency/AsyncTaskQueue.hpp
+++ b/Concurrency/AsyncTaskQueue.hpp
@@ -93,9 +93,7 @@ class DeferringAsyncTaskQueue: public AsyncTaskQueue {
 		void flush();
 
 	private:
-		// TODO: this is a shared_ptr because of the issues capturing moveables in C++11;
-		// switch to a unique_ptr if/when adapting to C++14
-		std::shared_ptr<std::list<std::function<void(void)>>> deferred_tasks_;
+		std::unique_ptr<std::list<std::function<void(void)>>> deferred_tasks_;
 };
 
 }

From 9d278d80f196b13cc39ead85a700b9b4d32c970f Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Thu, 2 Jun 2022 16:50:59 -0400
Subject: [PATCH 23/25] Remove redundant `reset`.

---
 Concurrency/AsyncTaskQueue.cpp | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Concurrency/AsyncTaskQueue.cpp b/Concurrency/AsyncTaskQueue.cpp
index 66b0fdaa3..be63ca571 100644
--- a/Concurrency/AsyncTaskQueue.cpp
+++ b/Concurrency/AsyncTaskQueue.cpp
@@ -96,7 +96,6 @@ void DeferringAsyncTaskQueue::defer(std::function<void(void)> function) {
 void DeferringAsyncTaskQueue::perform() {
 	if(!deferred_tasks_) return;
 	auto deferred_tasks_raw = deferred_tasks_.release();
-	deferred_tasks_.reset();
 	enqueue([deferred_tasks_raw] {
 		std::unique_ptr<std::list<std::function<void(void)>>> deferred_tasks(deferred_tasks_raw);
 		for(const auto &function : *deferred_tasks) {

From e389dcb9120de28b7a60d1d461bd0322eea54326 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Thu, 2 Jun 2022 16:52:03 -0400
Subject: [PATCH 24/25] Further simplify syntax.

---
 Concurrency/AsyncTaskQueue.cpp | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/Concurrency/AsyncTaskQueue.cpp b/Concurrency/AsyncTaskQueue.cpp
index be63ca571..38350d8df 100644
--- a/Concurrency/AsyncTaskQueue.cpp
+++ b/Concurrency/AsyncTaskQueue.cpp
@@ -95,8 +95,7 @@ void DeferringAsyncTaskQueue::defer(std::function<void(void)> function) {
 
 void DeferringAsyncTaskQueue::perform() {
 	if(!deferred_tasks_) return;
-	auto deferred_tasks_raw = deferred_tasks_.release();
-	enqueue([deferred_tasks_raw] {
+	enqueue([deferred_tasks_raw = deferred_tasks_.release()] {
 		std::unique_ptr<std::list<std::function<void(void)>>> deferred_tasks(deferred_tasks_raw);
 		for(const auto &function : *deferred_tasks) {
 			function();

From 7f33a5ca0c37e1b658372b702dafc345f7f55935 Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Thu, 2 Jun 2022 17:02:36 -0400
Subject: [PATCH 25/25] Simplify: (i) repetitive type for `TaskList`; (ii)
 unnecessary `unique_ptr`.

---
 Concurrency/AsyncTaskQueue.cpp | 66 +++++++++++++++++-----------------
 Concurrency/AsyncTaskQueue.hpp | 14 ++++----
 2 files changed, 41 insertions(+), 39 deletions(-)

diff --git a/Concurrency/AsyncTaskQueue.cpp b/Concurrency/AsyncTaskQueue.cpp
index 38350d8df..12615430e 100644
--- a/Concurrency/AsyncTaskQueue.cpp
+++ b/Concurrency/AsyncTaskQueue.cpp
@@ -12,47 +12,47 @@ using namespace Concurrency;
 
 AsyncTaskQueue::AsyncTaskQueue()
 #ifndef USE_GCD
-	: should_destruct_(false)
-#endif
-{
-#ifdef USE_GCD
-	serial_dispatch_queue_ = dispatch_queue_create("com.thomasharte.clocksignal.asyntaskqueue", DISPATCH_QUEUE_SERIAL);
+	:
+		should_destruct_(false),
+		thread_([this] () {
+			while(!should_destruct_) {
+				std::function<void(void)> next_function;
+
+				// Take lock, check for a new task.
+				std::unique_lock lock(queue_mutex_);
+				if(!pending_tasks_.empty()) {
+					next_function = pending_tasks_.front();
+					pending_tasks_.pop_front();
+				}
+
+				if(next_function) {
+					// If there is a task, release lock and perform it.
+					lock.unlock();
+					next_function();
+				} else {
+					// If there isn't a task, atomically block on the processing condition and release the lock
+					// until there's something pending (and then release it again via scope).
+					processing_condition_.wait(lock);
+				}
+			}
+		})
 #else
-	thread_ = std::make_unique<std::thread>([this]() {
-		while(!should_destruct_) {
-			std::function<void(void)> next_function;
-
-			// Take lock, check for a new task
-			std::unique_lock lock(queue_mutex_);
-			if(!pending_tasks_.empty()) {
-				next_function = pending_tasks_.front();
-				pending_tasks_.pop_front();
-			}
-
-			if(next_function) {
-				// If there is a task, release lock and perform it
-				lock.unlock();
-				next_function();
-			} else {
-				// If there isn't a task, atomically block on the processing condition and release the lock
-				// until there's something pending (and then release it again via scope)
-				processing_condition_.wait(lock);
-			}
-		}
-	});
+	: serial_dispatch_queue_(dispatch_queue_create("com.thomasharte.clocksignal.asyntaskqueue", DISPATCH_QUEUE_SERIAL))
 #endif
-}
+{}
 
 AsyncTaskQueue::~AsyncTaskQueue() {
 #ifdef USE_GCD
 	flush();
 	dispatch_release(serial_dispatch_queue_);
-	serial_dispatch_queue_ = nullptr;
 #else
+	// Set should destruct, and then give the thread a bit of a nudge
+	// via an empty enqueue.
 	should_destruct_ = true;
 	enqueue([](){});
-	thread_->join();
-	thread_.reset();
+
+	// Wait for the thread safely to terminate.
+	thread_.join();
 #endif
 }
 
@@ -88,7 +88,7 @@ DeferringAsyncTaskQueue::~DeferringAsyncTaskQueue() {
 
 void DeferringAsyncTaskQueue::defer(std::function<void(void)> function) {
 	if(!deferred_tasks_) {
-		deferred_tasks_ = std::make_unique<std::list<std::function<void(void)>>>();
+		deferred_tasks_ = std::make_unique<TaskList>();
 	}
 	deferred_tasks_->push_back(function);
 }
@@ -96,7 +96,7 @@ void DeferringAsyncTaskQueue::defer(std::function<void(void)> function) {
 void DeferringAsyncTaskQueue::perform() {
 	if(!deferred_tasks_) return;
 	enqueue([deferred_tasks_raw = deferred_tasks_.release()] {
-		std::unique_ptr<std::list<std::function<void(void)>>> deferred_tasks(deferred_tasks_raw);
+		std::unique_ptr<TaskList> deferred_tasks(deferred_tasks_raw);
 		for(const auto &function : *deferred_tasks) {
 			function();
 		}
diff --git a/Concurrency/AsyncTaskQueue.hpp b/Concurrency/AsyncTaskQueue.hpp
index 2962e329c..225f49580 100644
--- a/Concurrency/AsyncTaskQueue.hpp
+++ b/Concurrency/AsyncTaskQueue.hpp
@@ -23,6 +23,8 @@
 
 namespace Concurrency {
 
+using TaskList = std::list<std::function<void(void)>>;
+
 /*!
 	An async task queue allows a caller to enqueue void(void) functions. Those functions are guaranteed
 	to be performed serially and asynchronously from the caller. A caller may also request to flush,
@@ -51,12 +53,12 @@ class AsyncTaskQueue {
 #ifdef USE_GCD
 		dispatch_queue_t serial_dispatch_queue_;
 #else
-		std::unique_ptr<std::thread> thread_;
-
-		std::mutex queue_mutex_;
-		std::list<std::function<void(void)>> pending_tasks_;
-		std::condition_variable processing_condition_;
 		std::atomic_bool should_destruct_;
+		std::condition_variable processing_condition_;
+		std::mutex queue_mutex_;
+		TaskList pending_tasks_;
+
+		std::thread thread_;
 #endif
 };
 
@@ -93,7 +95,7 @@ class DeferringAsyncTaskQueue: public AsyncTaskQueue {
 		void flush();
 
 	private:
-		std::unique_ptr<std::list<std::function<void(void)>>> deferred_tasks_;
+		std::unique_ptr<TaskList> deferred_tasks_;
 };
 
 }