Skip to content

Commit 1df97c5

Browse files
committed
Add support for RZX file format
1 parent 1b55096 commit 1df97c5

29 files changed

Lines changed: 463 additions & 89 deletions

src/Spectron.Files/Szx/Extensions/ByteStreamReaderExtensions.cs renamed to src/Spectron.Files/Extensions/ByteStreamReaderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using System.Text;
22
using OldBit.Spectron.Files.IO;
33

4-
namespace OldBit.Spectron.Files.Szx.Extensions;
4+
namespace OldBit.Spectron.Files.Extensions;
55

66
internal static class ByteStreamReaderExtensions
77
{

src/Spectron.Files/Extensions/BytesExtensions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,17 @@ internal static class BytesExtensions
77
internal static string ToAsciiString(this IEnumerable<byte> bytes)
88
{
99
var s = new StringBuilder();
10+
1011
foreach (var b in bytes)
1112
{
13+
if (b == 0)
14+
{
15+
continue;
16+
}
17+
1218
s.Append((char)b);
1319
}
20+
1421
return s.ToString();
1522
}
1623
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System.Text;
2+
3+
namespace OldBit.Spectron.Files.Extensions;
4+
5+
internal static class StreamExtensions
6+
{
7+
extension(Stream stream)
8+
{
9+
internal void WriteWord(int value)
10+
{
11+
stream.WriteByte((byte)value);
12+
stream.WriteByte((byte)(value >> 8));
13+
}
14+
15+
internal void WriteDWord(DWord value)
16+
{
17+
stream.WriteByte((byte)value);
18+
stream.WriteByte((byte)(value >> 8));
19+
stream.WriteByte((byte)(value >> 16));
20+
stream.WriteByte((byte)(value >> 24));
21+
}
22+
23+
internal void WriteBytes(byte[] value) => stream.Write(value, 0, value.Length);
24+
25+
internal void WriteChars(string value, int length)
26+
{
27+
if (length == 0)
28+
{
29+
return;
30+
}
31+
32+
var bytes = Encoding.ASCII.GetBytes(value);
33+
stream.Write(bytes, 0, Math.Min(bytes.Length, length));
34+
35+
if (bytes.Length < length)
36+
{
37+
stream.Write(new byte[length - bytes.Length], 0, length - bytes.Length);
38+
}
39+
}
40+
}
41+
}

src/Spectron.Files/IO/ByteStreamReader.cs

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,54 +11,27 @@ internal sealed class ByteStreamReader
1111
/// Create a new instance of the byte reader.
1212
/// </summary>
1313
/// <param name="stream">The stream that provides data for the reader.</param>
14-
public ByteStreamReader(Stream stream)
15-
{
16-
_stream = stream;
17-
}
14+
public ByteStreamReader(Stream stream) => _stream = stream;
1815

1916
/// <summary>
2017
/// Reads a byte from the stream and advances the position within the stream by one byte.
2118
/// </summary>
2219
/// <returns>The byte retrieved from the stream.</returns>
2320
/// <exception cref="EndOfStreamException">Thrown when not enough data is in the stream.</exception>
24-
public byte ReadByte()
25-
{
26-
if (!TryReadByte( out var data))
27-
{
28-
throw new EndOfStreamException();
29-
}
30-
31-
return data;
32-
}
21+
public byte ReadByte() => !TryReadByte( out var data) ? throw new EndOfStreamException() : data;
3322

3423
/// <summary>
3524
/// Reads a word from the stream and advances the position within the stream by two bytes.
3625
/// </summary>
3726
/// <returns>The word retrieved from the stream.</returns>
3827
/// <exception cref="EndOfStreamException">Thrown when not enough data is in the stream.</exception>
39-
public Word ReadWord()
40-
{
41-
if (!TryReadWord(out var data))
42-
{
43-
throw new EndOfStreamException();
44-
}
45-
46-
return data;
47-
}
28+
public Word ReadWord() => !TryReadWord(out var data) ? throw new EndOfStreamException() : data;
4829

4930
/// <summary>
5031
/// Reads a dword from the stream and advances the position within the stream by four bytes.
5132
/// </summary>
5233
/// <returns>The dword retrieved from the stream.</returns>
53-
public DWord ReadDWord()
54-
{
55-
if (!TryReadDWord(out var data))
56-
{
57-
throw new EndOfStreamException();
58-
}
59-
60-
return data;
61-
}
34+
public DWord ReadDWord() => !TryReadDWord(out var data) ? throw new EndOfStreamException() : data;
6235

6336
/// <summary>
6437
/// Reads a sequence of bytes from the stream and advances the position within the stream by 'count' bytes.

src/Spectron.Files/IO/DataWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
using OldBit.Spectron.Files.Extensions;
12
using OldBit.Spectron.Files.Serialization;
2-
using OldBit.Spectron.Files.Szx.Extensions;
33
using OldBit.Spectron.Files.Tap;
44
using OldBit.Spectron.Files.Tzx.Blocks;
55

src/Spectron.Files/Rzx/BlockIds.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace OldBit.Spectron.Files.Rzx;
2+
3+
internal static class BlockIds
4+
{
5+
internal const byte Creator = 0x10;
6+
internal const byte Security = 0x20;
7+
internal const byte Signature = 0x21;
8+
internal const byte Snapshot = 0x30;
9+
internal const byte Recording = 0x80;
10+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using OldBit.Spectron.Files.IO;
2+
3+
namespace OldBit.Spectron.Files.Rzx.Blocks;
4+
5+
internal sealed class BlockHeader(byte blockId, DWord size)
6+
{
7+
internal byte BlockId { get; } = blockId;
8+
internal DWord Size { get; } = size;
9+
10+
internal static BlockHeader? Read(ByteStreamReader reader)
11+
{
12+
if (reader.TryReadByte(out var blockId) && reader.TryReadDWord(out var size))
13+
{
14+
return new BlockHeader(blockId, size);
15+
}
16+
17+
return null;
18+
}
19+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using OldBit.Spectron.Files.Extensions;
2+
using OldBit.Spectron.Files.IO;
3+
4+
namespace OldBit.Spectron.Files.Rzx.Blocks;
5+
6+
/// <summary>
7+
/// Information about the program which created the RZX.
8+
/// </summary>
9+
public class CreatorBlock
10+
{
11+
/// <summary>
12+
/// Creator's identification string.
13+
/// </summary>≠≠
14+
public string CreatorName { get; private set; } = string.Empty;
15+
16+
/// <summary>
17+
/// Creator's major version number.
18+
/// </summary>
19+
public Word MajorVersion { get; private set; }
20+
21+
/// <summary>
22+
/// Creator's minor version number.
23+
/// </summary>
24+
public Word MinorVersion { get; private set; }
25+
26+
/// <summary>
27+
/// Creator's custom data (may be absent).
28+
/// </summary>
29+
public byte[]? Data { get; private set; }
30+
31+
internal static CreatorBlock Read(ByteStreamReader reader, DWord blockLength)
32+
{
33+
var block = new CreatorBlock();
34+
35+
var bytes = reader.ReadBytes(20);
36+
block.CreatorName = bytes.ToAsciiString().Trim();
37+
block.MajorVersion = reader.ReadWord();
38+
block.MinorVersion = reader.ReadWord();
39+
block.Data = reader.ReadBytes((int)(blockLength - 29));
40+
41+
return block;
42+
}
43+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using OldBit.Spectron.Files.IO;
2+
using OldBit.Spectron.Files.Szx;
3+
4+
namespace OldBit.Spectron.Files.Rzx.Blocks;
5+
6+
/// <summary>
7+
/// Actual input recording data.
8+
/// </summary>
9+
public class RecordingBlock
10+
{
11+
/// <summary>
12+
/// Number of frames in the block.
13+
/// </summary>
14+
public DWord FrameCount { get; private init; }
15+
16+
/// <summary>
17+
/// Reserved.
18+
/// </summary>
19+
public byte Reserved { get; private set; }
20+
21+
/// <summary>
22+
/// T-STATES counter at the beginning.
23+
/// </summary>
24+
public DWord TStatesCounter { get; private set; }
25+
26+
/// <summary>
27+
/// Flags (b0: Protected (frames are encrypted with x-key), b1: Compressed data.)
28+
/// </summary>
29+
public DWord Flags { get; private set; }
30+
31+
public List<RecordingFrame> Frames { get; } = [];
32+
33+
internal static RecordingBlock Read(ByteStreamReader reader, DWord blockLength)
34+
{
35+
var block = new RecordingBlock
36+
{
37+
FrameCount = reader.ReadDWord(),
38+
Reserved = reader.ReadByte(),
39+
TStatesCounter = reader.ReadDWord(),
40+
Flags = reader.ReadDWord()
41+
};
42+
43+
var isProtected = (block.Flags & 0x01) == 0x01;
44+
if (isProtected)
45+
{
46+
throw new NotSupportedException("Protected recording blocks are not supported.");
47+
}
48+
49+
var data = reader.ReadBytes((int)(blockLength - 18));
50+
51+
var isCompressed = (block.Flags & 0x02) == 0x02;
52+
if (isCompressed)
53+
{
54+
data = ZLibHelper.Decompress(data);
55+
}
56+
57+
ReadFrames(block, data);
58+
59+
return block;
60+
}
61+
62+
private static void ReadFrames(RecordingBlock block, byte[] data)
63+
{
64+
var memoryStream = new MemoryStream(data);
65+
var reader = new ByteStreamReader(memoryStream);
66+
67+
for (var i = 0; i < block.FrameCount; i++)
68+
{
69+
var frame = RecordingFrame.Read(reader);
70+
71+
if (frame.InCounter == 65535)
72+
{
73+
// Repeated frame, copy the values from the previous frame
74+
frame.Values = block.Frames.LastOrDefault()?.Values ?? [];
75+
}
76+
77+
block.Frames.Add(frame);
78+
}
79+
}
80+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using OldBit.Spectron.Files.IO;
2+
3+
namespace OldBit.Spectron.Files.Rzx.Blocks;
4+
5+
/// <summary>
6+
/// Layout of the input log for the frame.
7+
/// </summary>
8+
public class RecordingFrame
9+
{
10+
/// <summary>
11+
/// Fetch counter till next interrupt (i.e. number of R increments, INTA excluded)
12+
/// </summary>
13+
public Word FetchCounter { get; private set; }
14+
15+
/// <summary>
16+
/// IN counter. Number of I/O port reads performed by the CPU in this frame (their return values follow).
17+
/// If equal to 65,535, this was a repeated frame, i.e. the port reads were exactly the same of the last frame.
18+
/// </summary>
19+
public Word InCounter { get; private set; }
20+
21+
/// <summary>
22+
/// Return values for the CPU I/O port reads.
23+
/// </summary>
24+
public byte[] Values { get; internal set; } = [];
25+
26+
internal static RecordingFrame Read(ByteStreamReader reader)
27+
{
28+
var block = new RecordingFrame
29+
{
30+
FetchCounter = reader.ReadWord(),
31+
InCounter = reader.ReadWord()
32+
};
33+
34+
if (block.InCounter != 65535)
35+
{
36+
block.Values = reader.ReadBytes(block.InCounter);
37+
}
38+
39+
return block;
40+
}
41+
}

0 commit comments

Comments
 (0)