using Core.Io; using System; using System.Collections.Generic; using System.IO; using static Core.Io.TurboSpeedBlock; 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; case 0x11: // Turbo Speed Data Block var turboBlock = new TurboSpeedBlock { PilotPulseLength = br.ReadUInt16(), SyncFirstPulseLength = br.ReadUInt16(), SyncSecondPulseLength = br.ReadUInt16(), ZeroBitPulseLength = br.ReadUInt16(), OneBitPulseLength = br.ReadUInt16(), PilotToneLength = br.ReadUInt16(), UsedBitsInLastByte = br.ReadByte(), PauseAfterMs = br.ReadUInt16() }; // Z80 files are Little-Endian. Read 3 bytes and combine them into a 24-bit integer. byte len0 = br.ReadByte(); byte len1 = br.ReadByte(); byte len2 = br.ReadByte(); turboBlock.DataLength = len0 | (len1 << 8) | (len2 << 16); // Read the actual tape data turboBlock.Data = br.ReadBytes(turboBlock.DataLength); blocks.Add(turboBlock); break; case 0x12: // Pure Tone Block blocks.Add(new PureToneBlock { PulseLength = br.ReadUInt16(), PulseCount = br.ReadUInt16() }); break; case 0x13: // Pulse Sequence Block var seqBlock = new PulseSequenceBlock(); byte rawCount = br.ReadByte(); // TZX Spec: A raw count of 0 means 256 pulses! seqBlock.PulseCount = (rawCount == 0) ? 256 : rawCount; seqBlock.Pulses = new ushort[seqBlock.PulseCount]; for (int i = 0; i < seqBlock.PulseCount; i++) { seqBlock.Pulses[i] = br.ReadUInt16(); } blocks.Add(seqBlock); break; case 0x14: // Pure Data Block var pureData = new PureDataBlock { ZeroBitPulseLength = br.ReadUInt16(), OneBitPulseLength = br.ReadUInt16(), UsedBitsInLastByte = br.ReadByte(), PauseAfterMs = br.ReadUInt16() }; // Assemble the 24-bit length byte pdLen0 = br.ReadByte(); byte pdLen1 = br.ReadByte(); byte pdLen2 = br.ReadByte(); pureData.DataLength = pdLen0 | (pdLen1 << 8) | (pdLen2 << 16); pureData.Data = br.ReadBytes(pureData.DataLength); blocks.Add(pureData); break; case 0x20: // Pause / Stop Tape Block blocks.Add(new PauseBlock { PauseDurationMs = br.ReadUInt16() }); break; case 0x21: // Group Start Block byte groupNameLength = br.ReadByte(); string groupName = System.Text.Encoding.ASCII.GetString(br.ReadBytes(groupNameLength)); blocks.Add(new GroupStartBlock { GroupName = groupName }); break; case 0x22: // Group End Block // This block has no payload data at all! Just the ID. blocks.Add(new GroupEndBlock()); break; case 0x30: // Text Description Block byte textLength30 = br.ReadByte(); string description = System.Text.Encoding.ASCII.GetString(br.ReadBytes(textLength30)); blocks.Add(new TextDescriptionBlock { Description = description }); break; case 0x32: // Archive Info Block var archiveBlock = new ArchiveInfoBlock(); // The total length of the block (we read it to advance the stream, // but the string count is what we actually use to loop) ushort totalLength = br.ReadUInt16(); byte stringCount = br.ReadByte(); for (int i = 0; i < stringCount; i++) { byte textId = br.ReadByte(); byte textLength32 = br.ReadByte(); // Read the raw bytes and convert them to an ASCII string string text = System.Text.Encoding.ASCII.GetString(br.ReadBytes(textLength32)); archiveBlock.Metadata[textId] = text; } blocks.Add(archiveBlock); 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; } } }