diff --git a/Components/9918/Implementation/9918.cpp b/Components/9918/Implementation/9918.cpp
index 715140843..628f1566a 100644
--- a/Components/9918/Implementation/9918.cpp
+++ b/Components/9918/Implementation/9918.cpp
@@ -218,9 +218,7 @@ void TMS9918<personality>::run_for(const HalfCycles cycles) {
 	// Convert 456 clocked half cycles per line to 342 internal cycles per line;
 	// the internal clock is 1.5 times the nominal 3.579545 Mhz that I've advertised
 	// for this part. So multiply by three quarters.
-	int int_cycles = int(cycles.as_integral() * 3) + this->cycles_error_;
-	this->cycles_error_ = int_cycles & 3;
-	int_cycles >>= 2;
+	int int_cycles = this->clock_converter_.to_internal(cycles.as<int>());
 	if(!int_cycles) return;
 
 	// There are two intertwined processes here, 'writing' (which means writing to the
@@ -236,7 +234,10 @@ void TMS9918<personality>::run_for(const HalfCycles cycles) {
 
 		if(write_cycles_pool) {
 			// Determine how much writing to do.
-			const int write_cycles = std::min(342 - this->write_pointer_.column, write_cycles_pool);
+			const int write_cycles = std::min(
+				this->clock_converter_.CyclesPerLine - this->write_pointer_.column,
+				write_cycles_pool
+			);
 			const int end_column = this->write_pointer_.column + write_cycles;
 			LineBuffer &line_buffer = this->line_buffers_[this->write_pointer_.row];
 
@@ -274,16 +275,16 @@ void TMS9918<personality>::run_for(const HalfCycles cycles) {
 			// Perform memory accesses.
 			// ------------------------
 #define fetch(function)	\
-	if(final_window != 171) {	\
+	if(final_window != this->clock_converter_.AccessWindowCyclesPerLine) {	\
 		function<true>(first_window, final_window);\
 	} else {\
 		function<false>(first_window, final_window);\
 	}
 
-			// column_ and end_column are in 342-per-line cycles;
-			// adjust them to a count of windows.
-			const int first_window = this->write_pointer_.column >> 1;
-			const int final_window = end_column >> 1;
+			// Adjust column_ and end_column to the access-window clock before calling
+			// the mode-applicable fetch function.
+			const int first_window = this->clock_converter_.to_access_clock(this->write_pointer_.column);
+			const int final_window = this->clock_converter_.to_access_clock(end_column);
 			if(first_window != final_window) {
 				switch(line_buffer.line_mode) {
 					case LineMode::Text:		fetch(this->template fetch_tms_text);		break;
@@ -336,7 +337,7 @@ void TMS9918<personality>::run_for(const HalfCycles cycles) {
 			this->write_pointer_.column = end_column;
 			write_cycles_pool -= write_cycles;
 
-			if(this->write_pointer_.column == 342) {
+			if(this->write_pointer_.column == this->clock_converter_.CyclesPerLine) {
 				this->write_pointer_.column = 0;
 				this->write_pointer_.row = (this->write_pointer_.row + 1) % this->mode_timing_.total_lines;
 				LineBuffer &next_line_buffer = this->line_buffers_[this->write_pointer_.row];
@@ -345,7 +346,7 @@ void TMS9918<personality>::run_for(const HalfCycles cycles) {
 				this->set_current_screen_mode();
 
 				// Based on the output mode, pick a line mode.
-				next_line_buffer.first_pixel_output_column = 86;
+				next_line_buffer.first_pixel_output_column = 86;	// TODO: these should be a function of ClockConverter::CyclesPerLine.
 				next_line_buffer.next_border_column = 342;
 				this->mode_timing_.maximum_visible_sprites = 4;
 				switch(this->screen_mode_) {
@@ -379,25 +380,31 @@ void TMS9918<personality>::run_for(const HalfCycles cycles) {
 
 		if(read_cycles_pool) {
 			// Determine how much time has passed in the remainder of this line, and proceed.
-			const int target_read_cycles = std::min(342 - this->read_pointer_.column, read_cycles_pool);
+			const int target_read_cycles = std::min(
+				this->clock_converter_.CyclesPerLine - this->read_pointer_.column,
+				read_cycles_pool
+			);
 			int read_cycles_performed = 0;
 			uint32_t next_cram_value = 0;
 
 			while(read_cycles_performed < target_read_cycles) {
+				int read_cycles = target_read_cycles - read_cycles_performed;
+				if(!read_cycles) continue;
+
 				const uint32_t cram_value = next_cram_value;
 				next_cram_value = 0;
-				int read_cycles = target_read_cycles - read_cycles_performed;
-				if(!this->upcoming_cram_dots_.empty() && this->upcoming_cram_dots_.front().location.row == this->read_pointer_.row) {
-					int time_until_dot = this->upcoming_cram_dots_.front().location.column - this->read_pointer_.column;
+				if constexpr (is_sega_vdp(personality)) {
+					if(!this->upcoming_cram_dots_.empty() && this->upcoming_cram_dots_.front().location.row == this->read_pointer_.row) {
+						int time_until_dot = this->upcoming_cram_dots_.front().location.column - this->read_pointer_.column;
 
-					if(time_until_dot < read_cycles) {
-						read_cycles = time_until_dot;
-						next_cram_value = this->upcoming_cram_dots_.front().value;
-						this->upcoming_cram_dots_.erase(this->upcoming_cram_dots_.begin());
+						if(time_until_dot < read_cycles) {
+							read_cycles = time_until_dot;
+							next_cram_value = this->upcoming_cram_dots_.front().value;
+							this->upcoming_cram_dots_.erase(this->upcoming_cram_dots_.begin());
+						}
 					}
 				}
 
-				if(!read_cycles) continue;
 				read_cycles_performed += read_cycles;
 
 				const int end_column = this->read_pointer_.column + read_cycles;
@@ -418,6 +425,7 @@ void TMS9918<personality>::run_for(const HalfCycles cycles) {
 
 #define border(left, right)	intersect(left, right, this->output_border(end - start, cram_value))
 
+				// TODO: CRT clock might need to change?
 				if(line_buffer.line_mode == LineMode::Refresh || this->read_pointer_.row > this->mode_timing_.pixel_lines) {
 					if(this->read_pointer_.row >= this->mode_timing_.first_vsync_line && this->read_pointer_.row < this->mode_timing_.first_vsync_line+4) {
 						// Vertical sync.
@@ -507,7 +515,7 @@ void TMS9918<personality>::run_for(const HalfCycles cycles) {
 			}
 
 			read_cycles_pool -= target_read_cycles;
-			if(this->read_pointer_.column == 342) {
+			if(this->read_pointer_.column == this->clock_converter_.CyclesPerLine) {
 				this->read_pointer_.column = 0;
 				this->read_pointer_.row = (this->read_pointer_.row + 1) % this->mode_timing_.total_lines;
 			}
@@ -750,11 +758,6 @@ uint8_t TMS9918<personality>::read(int address) {
 	return result;
 }
 
-template <Personality personality>
-HalfCycles Base<personality>::half_cycles_before_internal_cycles(int internal_cycles) const {
-	return HalfCycles(((internal_cycles << 2) + (2 - cycles_error_)) / 3);
-}
-
 template <Personality personality>
 HalfCycles TMS9918<personality>::get_next_sequence_point() const {
 	if(!this->generate_interrupts_ && !this->enable_line_interrupts_) return HalfCycles::max();
@@ -769,7 +772,7 @@ HalfCycles TMS9918<personality>::get_next_sequence_point() const {
 		) % frame_length;
 	if(!time_until_frame_interrupt) time_until_frame_interrupt = frame_length;
 
-	if(!this->enable_line_interrupts_) return this->half_cycles_before_internal_cycles(time_until_frame_interrupt);
+	if(!this->enable_line_interrupts_) return this->clock_converter_.half_cycles_before_internal_cycles(time_until_frame_interrupt);
 
 	// Calculate when the next line interrupt will occur.
 	int next_line_interrupt_row = -1;
@@ -797,17 +800,17 @@ HalfCycles TMS9918<personality>::get_next_sequence_point() const {
 	// the frame end interrupt or no interrupt pending as appropriate.
 	if(next_line_interrupt_row == -1) {
 		return this->generate_interrupts_ ?
-			this->half_cycles_before_internal_cycles(time_until_frame_interrupt) :
+			this->clock_converter_.half_cycles_before_internal_cycles(time_until_frame_interrupt) :
 			HalfCycles::max();
 	}
 
 	// Figure out the number of internal cycles until the next line interrupt, which is the amount
 	// of time to the next tick over and then next_line_interrupt_row - row_ lines further.
 	const int local_cycles_until_line_interrupt = cycles_to_next_interrupt_threshold + (next_line_interrupt_row - line_of_next_interrupt_threshold) * 342;
-	if(!this->generate_interrupts_) return this->half_cycles_before_internal_cycles(local_cycles_until_line_interrupt);
+	if(!this->generate_interrupts_) return this->clock_converter_.half_cycles_before_internal_cycles(local_cycles_until_line_interrupt);
 
 	// Return whichever interrupt is closer.
-	return this->half_cycles_before_internal_cycles(std::min(local_cycles_until_line_interrupt, time_until_frame_interrupt));
+	return this->clock_converter_.half_cycles_before_internal_cycles(std::min(local_cycles_until_line_interrupt, time_until_frame_interrupt));
 }
 
 template <Personality personality>
@@ -825,7 +828,7 @@ HalfCycles TMS9918<personality>::get_time_until_line(int line) {
 		line += this->mode_timing_.total_lines;
 	}
 
-	return this->half_cycles_before_internal_cycles(cycles_to_next_interrupt_threshold + (line - line_of_next_interrupt_threshold)*342);
+	return this->clock_converter_.half_cycles_before_internal_cycles(cycles_to_next_interrupt_threshold + (line - line_of_next_interrupt_threshold)*342);
 }
 
 template <Personality personality>
diff --git a/Components/9918/Implementation/9918Base.hpp b/Components/9918/Implementation/9918Base.hpp
index 396920ee9..93350cfcd 100644
--- a/Components/9918/Implementation/9918Base.hpp
+++ b/Components/9918/Implementation/9918Base.hpp
@@ -11,6 +11,7 @@
 
 #include "../../../Outputs/CRT/CRT.hpp"
 #include "../../../ClockReceiver/ClockReceiver.hpp"
+#include "ClockConverter.hpp"
 
 #include <array>
 #include <cassert>
@@ -199,11 +200,7 @@ template <Personality personality> struct Base {
 	uint8_t text_colour_ = 0;
 	uint8_t background_colour_ = 0;
 
-	// This implementation of this chip officially accepts a 3.58Mhz clock, but runs
-	// internally at 5.37Mhz. The following two help to maintain a lossless conversion
-	// from the one to the other.
-	int cycles_error_ = 0;
-	HalfCycles half_cycles_before_internal_cycles(int internal_cycles) const;
+	ClockConverter<personality> clock_converter_;
 
 	// Internal mechanisms for position tracking.
 	int latched_column_ = 0;
diff --git a/Components/9918/Implementation/ClockConverter.hpp b/Components/9918/Implementation/ClockConverter.hpp
new file mode 100644
index 000000000..da4b15c16
--- /dev/null
+++ b/Components/9918/Implementation/ClockConverter.hpp
@@ -0,0 +1,64 @@
+//
+//  ClockConverter.hpp
+//  Clock Signal
+//
+//  Created by Thomas Harte on 01/01/2023.
+//  Copyright © 2023 Thomas Harte. All rights reserved.
+//
+
+#ifndef ClockConverter_hpp
+#define ClockConverter_hpp
+
+#include "../9918.hpp"
+
+namespace TI {
+namespace TMS {
+
+template <Personality personality> class ClockConverter {
+	public:
+		/*!
+			Converts a number of **half-cycles** to an internal number
+			of **cycles**.
+		*/
+		int to_internal(int source) {
+			// Default behaviour is top apply a multiplication by 3/4.
+			const int result = source * 3 + cycles_error_;
+			cycles_error_ = result & 3;
+			return result >> 2;
+		}
+
+		/*!
+			Provides the number of external cycles that will need to pass in order to advance
+			_at least_ @c internal_cycles into the future.
+		*/
+		HalfCycles half_cycles_before_internal_cycles(int internal_cycles) const {
+			return HalfCycles(
+				((internal_cycles << 2) + (2 - cycles_error_)) / 3
+			);
+		}
+
+		/*
+			Converts a position in internal cycles to its corresponding position
+			on the access-window clock.
+		*/
+		static constexpr int to_access_clock(int source) {
+			return source >> 1;
+		}
+
+		/// The number of internal cycles in a single line.
+		constexpr static int CyclesPerLine = 342;
+
+		/// Indicates the number of access-window cycles in a single line.
+		constexpr static int AccessWindowCyclesPerLine = 171;
+
+	private:
+		// This implementation of this chip officially accepts a 3.58Mhz clock, but runs
+		// internally at 5.37Mhz. The following two help to maintain a lossless conversion
+		// from the one to the other.
+		int cycles_error_ = 0;
+};
+
+}
+}
+
+#endif /* ClockConverter_hpp */
diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj
index 595676063..ef9b2324a 100644
--- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
+++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
@@ -1317,6 +1317,7 @@
 		4B3FE75C1F3CF68B00448EE4 /* CPM.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CPM.cpp; path = Parsers/CPM.cpp; sourceTree = "<group>"; };
 		4B3FE75D1F3CF68B00448EE4 /* CPM.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = CPM.hpp; path = Parsers/CPM.hpp; sourceTree = "<group>"; };
 		4B43983829620FB1006B0BFC /* 9918.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = 9918.cpp; sourceTree = "<group>"; };
+		4B43983C29621024006B0BFC /* ClockConverter.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ClockConverter.hpp; sourceTree = "<group>"; };
 		4B448E7F1F1C45A00009ABD6 /* TZX.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TZX.cpp; sourceTree = "<group>"; };
 		4B448E801F1C45A00009ABD6 /* TZX.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TZX.hpp; sourceTree = "<group>"; };
 		4B448E821F1C4C480009ABD6 /* PulseQueuedTape.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PulseQueuedTape.cpp; sourceTree = "<group>"; };
@@ -4734,6 +4735,7 @@
 			children = (
 				4B43983829620FB1006B0BFC /* 9918.cpp */,
 				4BD388411FE34E010042B588 /* 9918Base.hpp */,
+				4B43983C29621024006B0BFC /* ClockConverter.hpp */,
 			);
 			path = Implementation;
 			sourceTree = "<group>";