From bf3d7d228a0a17b905b90a7bd5bff90d33dd2db2 Mon Sep 17 00:00:00 2001 From: Marc Parsons Date: Thu, 30 Apr 2026 17:43:20 +0100 Subject: [PATCH] WIP implementing TZX support --- Core/Io/TapManager.cs | 10 ++++++++ Core/Io/TzxBlocks.cs | 24 ++++++++++++++++++ Core/Io/TzxParser.cs | 56 +++++++++++++++++++++++++++++++++++++++++ Core/SpectrumMachine.cs | 3 +++ Desktop/Form1.cs | 8 ++++++ 5 files changed, 101 insertions(+) create mode 100644 Core/Io/TzxBlocks.cs create mode 100644 Core/Io/TzxParser.cs diff --git a/Core/Io/TapManager.cs b/Core/Io/TapManager.cs index 64d7c94..87d6ac0 100644 --- a/Core/Io/TapManager.cs +++ b/Core/Io/TapManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Core.Io; namespace Core.Io { @@ -7,6 +8,7 @@ namespace Core.Io { private Queue _blocks = new Queue(); private byte[] _currentBlock; + private List _tzxBlocks = new List(); // State Machine Tracking private enum TapeState { Idle, Pilot, Sync1, Sync2, Data, Pause } @@ -204,5 +206,13 @@ namespace Core.Io _currentBlock = null; return _blocks.Count > 0 ? _blocks.Dequeue() : null; } + + public void LoadTzxData(List blocks) + { + _tzxBlocks = blocks; + + // Just to prove it works before we build the State Machine! + System.Diagnostics.Debug.WriteLine($"Successfully loaded {_tzxBlocks.Count} TZX blocks!"); + } } } \ No newline at end of file diff --git a/Core/Io/TzxBlocks.cs b/Core/Io/TzxBlocks.cs new file mode 100644 index 0000000..1fc212c --- /dev/null +++ b/Core/Io/TzxBlocks.cs @@ -0,0 +1,24 @@ +using System; + +namespace Core.Io +{ + // The base class all future TZX blocks will inherit from + public abstract class TzxBlock + { + public abstract int BlockId { get; } + } + + // ID 0x10: Standard Speed Data Block + public class StandardSpeedBlock : TzxBlock + { + public override int BlockId => 0x10; + + // How long to pause the tape in milliseconds after this block loads + public ushort PauseAfterMs { get; set; } + + public ushort DataLength { get; set; } + + // The actual tape bytes + public byte[] Data { get; set; } = Array.Empty(); + } +} \ No newline at end of file diff --git a/Core/Io/TzxParser.cs b/Core/Io/TzxParser.cs new file mode 100644 index 0000000..659f06f --- /dev/null +++ b/Core/Io/TzxParser.cs @@ -0,0 +1,56 @@ +using Core.Io; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Core.Io +{ + public class TzxParser + { + public static List Parse(byte[] fileBytes) + { + List blocks = new List(); + + using (MemoryStream ms = new MemoryStream(fileBytes)) + using (BinaryReader br = new BinaryReader(ms)) + { + // 1. Verify the 10-Byte Header + // Signature is "ZXTape!" followed by EOF (0x1A) + byte[] signature = br.ReadBytes(8); + string sigString = System.Text.Encoding.ASCII.GetString(signature); + + if (sigString != "ZXTape!\x1A") + throw new Exception("Invalid TZX Signature! Is this a real TZX file?"); + + byte majorVersion = br.ReadByte(); + byte minorVersion = br.ReadByte(); + + // 2. Read the blocks until we hit the end of the file + while (ms.Position < ms.Length) + { + byte blockId = br.ReadByte(); + + switch (blockId) + { + case 0x10: // Standard Speed Data + var stdBlock = new StandardSpeedBlock + { + PauseAfterMs = br.ReadUInt16(), + DataLength = br.ReadUInt16() + }; + stdBlock.Data = br.ReadBytes(stdBlock.DataLength); + blocks.Add(stdBlock); + break; + + // TODO: Add cases for 0x11 (Turbo), 0x12 (Tone), 0x13 (Pulses), etc. + + default: + throw new NotImplementedException($"TZX Block ID 0x{blockId:X2} is not supported yet!"); + } + } + } + + return blocks; + } + } +} \ No newline at end of file diff --git a/Core/SpectrumMachine.cs b/Core/SpectrumMachine.cs index c37c928..f670596 100644 --- a/Core/SpectrumMachine.cs +++ b/Core/SpectrumMachine.cs @@ -23,6 +23,7 @@ namespace Core public IO_Bus IoBus { get; private set; } public ULA Ula { get; private set; } public TapManager TapeDeck { get; private set; } + public TzxParser _tzxParser { get; set; } // Audio (If BeeperDevice is in Desktop, you may need to move it to Core, // or inject it via an interface later. For now, assuming it's accessible). @@ -276,6 +277,8 @@ namespace Core ForceRet(); } + + private void ForceRet() { byte pcLow = Memory.Read(Cpu.SP); diff --git a/Desktop/Form1.cs b/Desktop/Form1.cs index c5be012..240ffef 100644 --- a/Desktop/Form1.cs +++ b/Desktop/Form1.cs @@ -392,6 +392,14 @@ namespace Desktop { _machine.LoadSnapshot(fileBytes); } + else if (resourceName.EndsWith(".tzx", StringComparison.OrdinalIgnoreCase)) + { + // 1. Pass the raw bytes to our static parser + List tzxBlocks = TzxParser.Parse(fileBytes); + + // 2. Hand the cleanly parsed blocks over to the Tape Deck + _machine.TapeDeck.LoadTzxData(tzxBlocks); + } } } }