162 lines
7.4 KiB
C#
162 lines
7.4 KiB
C#
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<TzxBlock> Parse(byte[] fileBytes)
|
|
{
|
|
List<TzxBlock> blocks = new List<TzxBlock>();
|
|
|
|
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;
|
|
}
|
|
}
|
|
} |