BizHawk/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TzxSerializer.cs

1633 lines
64 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// Reponsible for TZX format serializaton
/// </summary>
public class TzxSerializer : MediaSerializer
{
/// <summary>
/// The type of serializer
/// </summary>
private MediaSerializationType _formatType = MediaSerializationType.TZX;
public override MediaSerializationType FormatType
{
get
{
return _formatType;
}
}
/// <summary>
/// Signs whether this class can be used to serialize
/// </summary>
public override bool IsSerializer { get { return false; } }
/// <summary>
/// Signs whether this class can be used to de-serialize
/// </summary>
public override bool IsDeSerializer { get { return true; } }
/// <summary>
/// Working list of generated tape data blocks
/// </summary>
private List<TapeDataBlock> _blocks = new List<TapeDataBlock>();
/// <summary>
/// Position counter
/// </summary>
private int _position = 0;
/// <summary>
/// Object to keep track of loops - this assumes there is only one loop at a time
/// </summary>
private List<KeyValuePair<int, int>> _loopCounter = new List<KeyValuePair<int, int>>();
#region Construction
private DatacorderDevice _datacorder;
public TzxSerializer(DatacorderDevice _tapeDevice)
{
_datacorder = _tapeDevice;
}
#endregion
/// <summary>
/// DeSerialization method
/// </summary>
/// <param name="data"></param>
public override void DeSerialize(byte[] data)
{
// clear existing tape blocks
_datacorder.DataBlocks.Clear();
/*
// TZX Header
length: 10 bytes
Offset Value Type Description
0x00 "ZXTape!" ASCII[7] TZX signature
0x07 0x1A BYTE End of text file marker
0x08 1 BYTE TZX major revision number
0x09 20 BYTE TZX minor revision number
*/
// check whether this is a valid tzx format file by looking at the identifier in the header
// (first 7 bytes of the file)
string ident = Encoding.ASCII.GetString(data, 0, 7);
// and 'end of text' marker
byte eotm = data[7];
// version info
int majorVer = data[8];
int minorVer = data[9];
if (ident != "ZXTape!" || eotm != 0x1A)
{
// this is not a valid TZX format file
throw new Exception(this.GetType().ToString() +
"This is not a valid TZX format file");
}
// iterate through each block
_position = 10;
while (_position < data.Length)
{
// block ID is the first byte in a new block
int ID = data[_position++];
// process the data
ProcessBlock(data, ID);
}
}
/// <summary>
/// Processes a TZX block
/// </summary>
/// <param name="data"></param>
/// <param name="id"></param>
private void ProcessBlock(byte[] data, int id)
{
// process based on detected block ID
switch (id)
{
// ID 10 - Standard Speed Data Block
case 0x10:
ProcessBlockID10(data);
break;
// ID 11 - Turbo Speed Data Block
case 0x11:
ProcessBlockID11(data);
break;
// ID 12 - Pure Tone
case 0x12:
ProcessBlockID12(data);
break;
// ID 13 - Pulse sequence
case 0x13:
ProcessBlockID13(data);
break;
// ID 14 - Pure Data Block
case 0x14:
ProcessBlockID14(data);
break;
// ID 15 - Direct Recording
case 0x15:
ProcessBlockID15(data);
break;
// ID 18 - CSW Recording
case 0x18:
ProcessBlockID18(data);
break;
// ID 19 - Generalized Data Block
case 0x19:
ProcessBlockID19(data);
break;
// ID 20 - Pause (silence) or 'Stop the Tape' command
case 0x20:
ProcessBlockID20(data);
break;
// ID 21 - Group start
case 0x21:
ProcessBlockID21(data);
break;
// ID 22 - Group end
case 0x22:
ProcessBlockID22(data);
break;
// ID 23 - Jump to block
case 0x23:
ProcessBlockID23(data);
break;
// ID 24 - Loop start
case 0x24:
ProcessBlockID24(data);
break;
// ID 25 - Loop end
case 0x25:
ProcessBlockID25(data);
break;
// ID 26 - Call sequence
case 0x26:
ProcessBlockID26(data);
break;
// ID 27 - Return from sequence
case 0x27:
ProcessBlockID27(data);
break;
// ID 28 - Select block
case 0x28:
ProcessBlockID28(data);
break;
// ID 2A - Stop the tape if in 48K mode
case 0x2A:
ProcessBlockID2A(data);
break;
// ID 2B - Set signal level
case 0x2B:
ProcessBlockID2B(data);
break;
// ID 30 - Text description
case 0x30:
ProcessBlockID30(data);
break;
// ID 31 - Message block
case 0x31:
ProcessBlockID31(data);
break;
// ID 32 - Archive info
case 0x32:
ProcessBlockID32(data);
break;
// ID 33 - Hardware type
case 0x33:
ProcessBlockID33(data);
break;
// ID 35 - Custom info block
case 0x35:
ProcessBlockID35(data);
break;
// ID 5A - "Glue" block
case 0x5A:
ProcessBlockID5A(data);
break;
#region Depreciated Blocks
// ID 16 - C64 ROM Type Data Block
case 0x16:
ProcessBlockID16(data);
break;
// ID 17 - C64 Turbo Tape Data Block
case 0x17:
ProcessBlockID17(data);
break;
// ID 34 - Emulation info
case 0x34:
ProcessBlockID34(data);
break;
// ID 40 - Snapshot block
case 0x40:
ProcessBlockID40(data);
break;
#endregion
default:
ProcessUnidentifiedBlock(data);
break;
}
}
#region TZX Block Processors
#region ID 10 - Standard Speed Data Block
/* length: [02,03]+04
Offset Value Type Description
0x00 - WORD Pause after this block (ms.) {1000}
0x02 N WORD Length of data that follow
0x04 - BYTE[N] Data as in .TAP files
This block must be replayed with the standard Spectrum ROM timing values - see the values in
curly brackets in block ID 11. The pilot tone consists in 8063 pulses if the first data byte
(flag byte) is < 128, 3223 otherwise. This block can be used for the ROM loading routines AND
for custom loading routines that use the same timings as ROM ones do. */
private void ProcessBlockID10(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x10;
t.BlockDescription = BlockType.Standard_Speed_Data_Block;
t.DataPeriods = new List<int>();
int pauseLen = GetWordValue(data, _position);
if (pauseLen == 0)
pauseLen = 1000;
int blockLen = GetWordValue(data, _position + 2);
_position += 4;
byte[] tmp = new byte[blockLen];
tmp = data.Skip(_position).Take(blockLen).ToArray();
var t2 = DecodeDataBlock(t, tmp, DataBlockType.Standard, pauseLen);
// add the block
_datacorder.DataBlocks.Add(t2);
// advance the position to the next block
_position += blockLen;
}
#endregion
#region ID 11 - Turbo Speed Data Block
/* length: [0F,10,11]+12
Offset Value Type Description
0x00 - WORD Length of PILOT pulse {2168}
0x02 - WORD Length of SYNC first pulse {667}
0x04 - WORD Length of SYNC second pulse {735}
0x06 - WORD Length of ZERO bit pulse {855}
0x08 - WORD Length of ONE bit pulse {1710}
0x0A - WORD Length of PILOT tone (number of pulses) {8063 header (flag<128), 3223 data (flag>=128)}
0x0C - BYTE Used bits in the last byte (other bits should be 0) {8}
(e.g. if this is 6, then the bits used (x) in the last byte are: xxxxxx00,
where MSb is the leftmost bit, LSb is the rightmost bit)
0x0D - WORD Pause after this block (ms.) {1000}
0x0F N BYTE[3] Length of data that follow
0x12 - BYTE[N] Data as in .TAP files
This block is very similar to the normal TAP block but with some additional info on the timings and other important
differences. The same tape encoding is used as for the standard speed data block. If a block should use some non-standard
sync or pilot tones (i.e. all sorts of protection schemes) then use the next three blocks to describe it.*/
private void ProcessBlockID11(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x11;
t.BlockDescription = BlockType.Turbo_Speed_Data_Block;
t.DataPeriods = new List<int>();
int pilotPL = GetWordValue(data, _position);
int sync1P = GetWordValue(data, _position + 2);
int sync2P = GetWordValue(data, _position + 4);
int bit0P = GetWordValue(data, _position + 6);
int bit1P = GetWordValue(data, _position + 8);
int pilotTL = GetWordValue(data, _position + 10);
int bitinbyte = data[_position + 12];
int pause = GetWordValue(data, _position + 13);
int blockLen = 0xFFFFFF & GetInt32(data, _position + 0x0F);
_position += 0x12;
byte[] tmp = new byte[blockLen];
tmp = data.Skip(_position).Take(blockLen).ToArray();
var t2 = DecodeDataBlock(t, tmp, DataBlockType.Turbo, pause, pilotTL, pilotPL, sync1P, sync2P, bit0P, bit1P, bitinbyte);
// add the block
_datacorder.DataBlocks.Add(t2);
// advance the position to the next block
_position += blockLen;
}
#endregion
#region ID 12 - Pure Tone
/* length: 04
Offset Value Type Description
0x00 - WORD Length of one pulse in T-states
0x02 - WORD Number of pulses
This will produce a tone which is basically the same as the pilot tone in the ID 10, ID 11 blocks. You can define how
long the pulse is and how many pulses are in the tone. */
private void ProcessBlockID12(byte[] data)
{
int blockLen = 4;
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x12;
t.BlockDescription = BlockType.Pure_Tone;
t.DataPeriods = new List<int>();
// get values
int pulseLength = GetWordValue(data, _position);
int pulseCount = GetWordValue(data, _position + 2);
t.AddMetaData(BlockDescriptorTitle.Pulse_Length, pulseLength.ToString() + " T-States");
t.AddMetaData(BlockDescriptorTitle.Pulse_Count, pulseCount.ToString());
// build period information
for (int p = 0; p < pulseCount; p++)
{
t.DataPeriods.Add(pulseLength);
}
// add the block
_datacorder.DataBlocks.Add(t);
// advance the position to the next block
_position += blockLen;
}
#endregion
#region ID 13 - Pulse sequence
/* length: [00]*02+01
Offset Value Type Description
0x00 N BYTE Number of pulses
0x01 - WORD[N] Pulses' lengths
This will produce N pulses, each having its own timing. Up to 255 pulses can be stored in this block; this is useful for non-standard
sync tones used by some protection schemes. */
private void ProcessBlockID13(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x13;
t.BlockDescription = BlockType.Pulse_Sequence;
t.DataPeriods = new List<int>();
// get pulse count
int pulseCount = data[_position];
t.AddMetaData(BlockDescriptorTitle.Pulse_Count, pulseCount.ToString());
_position++;
// build period information
for (int p = 0; p < pulseCount; p++, _position += 2)
{
// get pulse length
int pulseLength = GetWordValue(data, _position);
t.AddMetaData(BlockDescriptorTitle.Needs_Parsing, "Pulse " + p + " Length\t" + pulseLength.ToString() + " T-States");
t.DataPeriods.Add(pulseLength);
}
// add the block
_datacorder.DataBlocks.Add(t);
}
#endregion
#region ID 14 - Pure Data Block
/* length: [07,08,09]+0A
Offset Value Type Description
0x00 - WORD Length of ZERO bit pulse
0x02 - WORD Length of ONE bit pulse
0x04 - BYTE Used bits in last byte (other bits should be 0)
(e.g. if this is 6, then the bits used (x) in the last byte are: xxxxxx00,
where MSb is the leftmost bit, LSb is the rightmost bit)
0x05 - WORD Pause after this block (ms.)
0x07 N BYTE[3] Length of data that follow
0x0A - BYTE[N] Data as in .TAP files
This is the same as in the turbo loading data block, except that it has no pilot or sync pulses. */
private void ProcessBlockID14(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x14;
t.BlockDescription = BlockType.Pure_Data_Block;
t.DataPeriods = new List<int>();
int pilotPL = 0;
int sync1P = 0;
int sync2P = 0;
int bit0P = GetWordValue(data, _position + 0);
int bit1P = GetWordValue(data, _position + 2);
int pilotTL = 0;
int bitinbyte = data[_position + 4];
int pause = GetWordValue(data, _position + 5);
int blockLen = 0xFFFFFF & GetInt32(data, _position + 0x07);
_position += 0x0A;
byte[] tmp = new byte[blockLen];
tmp = data.Skip(_position).Take(blockLen).ToArray();
var t2 = DecodeDataBlock(t, tmp, DataBlockType.Pure, pause, pilotTL, pilotPL, sync1P, sync2P, bit0P, bit1P, bitinbyte);
// add the block
_datacorder.DataBlocks.Add(t2);
// advance the position to the next block
_position += blockLen;
}
#endregion
#region ID 15 - Direct Recording
/* length: [05,06,07]+08
Offset Value Type Description
0x00 - WORD Number of T-states per sample (bit of data)
0x02 - WORD Pause after this block in milliseconds (ms.)
0x04 - BYTE Used bits (samples) in last byte of data (1-8)
(e.g. if this is 2, only first two samples of the last byte will be played)
0x05 N BYTE[3] Length of samples' data
0x08 - BYTE[N] Samples data. Each bit represents a state on the EAR port (i.e. one sample).
MSb is played first.
This block is used for tapes which have some parts in a format such that the turbo loader block cannot be used.
This is not like a VOC file, since the information is much more compact. Each sample value is represented by one bit only
(0 for low, 1 for high) which means that the block will be at most 1/8 the size of the equivalent VOC.
The preferred sampling frequencies are 22050 or 44100 Hz (158 or 79 T-states/sample).
Please, if you can, don't use other sampling frequencies.
Please use this block only if you cannot use any other block. */
private void ProcessBlockID15(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x15;
t.BlockDescription = BlockType.Direct_Recording;
t.DataPeriods = new List<int>();
// get values
int samLen = GetInt32(data, _position + 5);
int samSize = 0xFFFFFF & samLen;
int tStatesPerSample = GetWordValue(data, _position);
int pauseAfterBlock = GetWordValue(data, _position + 2);
int usedBitsInLastByte = data[_position + 4];
// skip to samples data
_position += 8;
int pulseLength = 0;
int pulseCount = 0;
// ascertain the pulse count
for (int i = 0; i < samSize; i++)
{
for (int p = 0x80; p != 0; p >>= 1)
{
if (((data[_position + i] ^ pulseLength) & p) != 0)
{
pulseCount++;
pulseLength ^= -1;
}
}
}
// get the pulses
t.DataPeriods = new List<int>(pulseCount + 2);
int tStateCount = 0;
pulseLength = 0;
for (int i = 1; i < samSize; i++)
{
for (int p = 0x80; p != 0; p >>= 1)
{
tStateCount += tStatesPerSample;
if (((data[_position] ^ pulseLength) & p) != 0)
{
t.DataPeriods.Add(tStateCount);
pulseLength ^= -1;
tStateCount = 0;
}
}
// incrememt position
_position++;
}
// get the pulses in the last byte of data
for (int p = 0x80; p != (byte)(0x80 >> usedBitsInLastByte); p >>= 1)
{
tStateCount += tStatesPerSample;
if (((data[_position] ^ pulseLength) & p) != 0)
{
t.DataPeriods.Add(tStateCount);
pulseLength ^= -1;
tStateCount = 0;
}
}
// add final pulse
t.DataPeriods.Add(tStateCount);
// add end of block pause
if (pauseAfterBlock > 0)
{
t.DataPeriods.Add(3500 * pauseAfterBlock);
}
// increment position
_position++;
// add the block
_datacorder.DataBlocks.Add(t);
}
#endregion
#region ID 18 - CSW Recording
/* length: [00,01,02,03]+04
Offset Value Type Description
0x00 10+N DWORD Block length (without these four bytes)
0x04 - WORD Pause after this block (in ms).
0x06 - BYTE[3] Sampling rate
0x09 - BYTE Compression type
0x01: RLE
0x02: Z-RLE
0x0A - DWORD Number of stored pulses (after decompression, for validation purposes)
0x0E - BYTE[N] CSW data, encoded according to the CSW file format specification.
This block contains a sequence of raw pulses encoded in CSW format v2 (Compressed Square Wave). */
private void ProcessBlockID18(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x18;
t.BlockDescription = BlockType.CSW_Recording;
t.DataPeriods = new List<int>();
// add the block
_datacorder.DataBlocks.Add(t);
}
#endregion
#region ID 19 - Generalized Data Block
/* length: [00,01,02,03]+04
Offset Value Type Description
0x00 - DWORD Block length (without these four bytes)
0x04 - WORD Pause after this block (ms)
0x06 TOTP DWORD Total number of symbols in pilot/sync block (can be 0)
0x0A NPP BYTE Maximum number of pulses per pilot/sync symbol
0x0B ASP BYTE Number of pilot/sync symbols in the alphabet table (0=256)
0x0C TOTD DWORD Total number of symbols in data stream (can be 0)
0x10 NPD BYTE Maximum number of pulses per data symbol
0x11 ASD BYTE Number of data symbols in the alphabet table (0=256)
0x12 - SYMDEF[ASP] Pilot and sync symbols definition table
This field is present only if TOTP>0
0x12+
(2*NPP+1)*ASP - PRLE[TOTP] Pilot and sync data stream
This field is present only if TOTP>0
0x12+
(TOTP>0)*((2*NPP+1)*ASP)+
TOTP*3 - SYMDEF[ASD] Data symbols definition table
This field is present only if TOTD>0
0x12+
(TOTP>0)*((2*NPP+1)*ASP)+
TOTP*3+
(2*NPD+1)*ASD - BYTE[DS] Data stream
This field is present only if TOTD>0
This block has been specifically developed to represent an extremely wide range of data encoding techniques.
The basic idea is that each loading component (pilot tone, sync pulses, data) is associated to a specific sequence
of pulses, where each sequence (wave) can contain a different number of pulses from the others.
In this way we can have a situation where bit 0 is represented with 4 pulses and bit 1 with 8 pulses.
----
SYMDEF structure format
Offset Value Type Description
0x00 - BYTE Symbol flags
b0-b1: starting symbol polarity
00: opposite to the current level (make an edge, as usual) - default
01: same as the current level (no edge - prolongs the previous pulse)
10: force low level
11: force high level
0x01 - WORD[MAXP] Array of pulse lengths.
The alphabet is stored using a table where each symbol is a row of pulses. The number of columns (i.e. pulses) of the table is the
length of the longest sequence amongst all (MAXP=NPP or NPD, for pilot/sync or data blocks respectively); shorter waves are terminated by a
zero-length pulse in the sequence.
Any number of data symbols is allowed, so we can have more than two distinct waves; for example, imagine a loader which writes two bits at a
time by encoding them with four distinct pulse lengths: this loader would have an alphabet of four symbols, each associated to a specific
sequence of pulses (wave).
----
----
PRLE structure format
Offset Value Type Description
0x00 - BYTE Symbol to be represented
0x01 - WORD Number of repetitions
Most commonly, pilot and sync are repetitions of the same pulse, thus they are represented using a very simple RLE encoding structure which stores
the symbol and the number of times it must be repeated.
Each symbol in the data stream is represented by a string of NB bits of the block data, where NB = ceiling(Log2(ASD)).
Thus the length of the whole data stream in bits is NB*TOTD, or in bytes DS=ceil(NB*TOTD/8).
---- */
private void ProcessBlockID19(byte[] data)
{
string test = "dgfg";
}
#endregion
#region ID 20 - Pause (silence) or 'Stop the Tape' command
/* length: 02
Offset Value Type Description
0x00 - WORD Pause duration (ms.)
This will make a silence (low amplitude level (0)) for a given time in milliseconds. If the value is 0 then the
emulator or utility should (in effect) STOP THE TAPE, i.e. should not continue loading until the user or emulator requests it. */
private void ProcessBlockID20(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x20;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Pause_or_Stop_the_Tape;
int pauseDuration = GetWordValue(data, _position);
if (pauseDuration != 0)
{
//t.BlockDescription = "Pause: " + pauseDuration + " ms";
}
else
{
//t.BlockDescription = "[STOP THE TAPE]";
}
if (pauseDuration == 0)
{
// issue stop the tape command
t.Command = TapeCommand.STOP_THE_TAPE;
// add 1ms period
t.DataPeriods.Add(3500);
pauseDuration = -1;
}
else
{
// this is actually just a pause
pauseDuration = 3500 * pauseDuration;
}
// add end of block pause
t.DataPeriods.Add(pauseDuration);
// add to tape
_datacorder.DataBlocks.Add(t);
// advanced position to next block
_position += 2;
}
#endregion
#region ID 21 - Group start
/* length: [00]+01
Offset Value Type Description
0x00 L BYTE Length of the group name string
0x01 - CHAR[L] Group name in ASCII format (please keep it under 30 characters long)
This block marks the start of a group of blocks which are to be treated as one single (composite) block.
This is very handy for tapes that use lots of subblocks like Bleepload (which may well have over 160 custom loading blocks).
You can also give the group a name (example 'Bleepload Block 1').
For each group start block, there must be a group end block. Nesting of groups is not allowed. */
private void ProcessBlockID21(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x21;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Group_Start;
int nameLength = data[_position];
_position++;
string name = Encoding.ASCII.GetString(data, _position, nameLength);
//t.BlockDescription = "[GROUP: " + name + "]";
t.Command = TapeCommand.BEGIN_GROUP;
// add to tape
_datacorder.DataBlocks.Add(t);
// advance to next block
_position += nameLength;
}
#endregion
#region ID 22 - Group end
/* length: 00
This indicates the end of a group. This block has no body. */
private void ProcessBlockID22(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x22;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Group_End;
t.Command = TapeCommand.END_GROUP;
// add to tape
_datacorder.DataBlocks.Add(t);
}
#endregion
#region ID 23 - Jump to block
/* length: 02
Offset Value Type Description
0x00 - WORD Relative jump value
This block will enable you to jump from one block to another within the file. The value is a signed short word
(usually 'signed short' in C); Some examples:
Jump 0 = 'Loop Forever' - this should never happen
Jump 1 = 'Go to the next block' - it is like NOP in assembler ;)
Jump 2 = 'Skip one block'
Jump -1 = 'Go to the previous block'
All blocks are included in the block count!. */
private void ProcessBlockID23(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x23;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Jump_to_Block;
int relativeJumpValue = GetWordValue(data, _position);
string result = string.Empty;
switch(relativeJumpValue)
{
case 0:
result = "Loop Forever";
break;
case 1:
result = "To Next Block";
break;
case 2:
result = "Skip One Block";
break;
case -1:
result = "Go to Previous Block";
break;
}
//t.BlockDescription = "[JUMP BLOCK - " + result +"]";
// add to tape
_datacorder.DataBlocks.Add(t);
// advance to next block
_position += 2;
}
#endregion
#region ID 24 - Loop start
/* length: 02
Offset Value Type Description
0x00 - WORD Number of repetitions (greater than 1)
If you have a sequence of identical blocks, or of identical groups of blocks, you can use this block to tell how many times they should
be repeated. This block is the same as the FOR statement in BASIC.
For simplicity reasons don't nest loop blocks! */
private void ProcessBlockID24(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x24;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Loop_Start;
// loop should start from the next block
int loopStart = _datacorder.DataBlocks.Count() + 1;
int numberOfRepetitions = GetWordValue(data, _position);
// update loop counter
_loopCounter.Add(
new KeyValuePair<int, int>(
loopStart,
numberOfRepetitions));
// update description
//t.BlockDescription = "[LOOP START - " + numberOfRepetitions + " times]";
// add to tape
_datacorder.DataBlocks.Add(t);
// advance to next block
_position += 2;
}
#endregion
#region ID 25 - Loop end
/* length: 00
This is the same as BASIC's NEXT statement. It means that the utility should jump back to the start of the loop if it hasn't
been run for the specified number of times.
This block has no body. */
private void ProcessBlockID25(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x25;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Loop_End;
// get the most recent loop info
var loop = _loopCounter.LastOrDefault();
int loopStart = loop.Key;
int numberOfRepetitions = loop.Value;
if (numberOfRepetitions == 0)
{
return;
}
// get the number of blocks to loop
int blockCnt = _datacorder.DataBlocks.Count() - loopStart;
// loop through each group to repeat
for (int b = 0; b < numberOfRepetitions; b++)
{
TapeDataBlock repeater = new TapeDataBlock();
//repeater.BlockDescription = "[LOOP REPEAT - " + (b + 1) + "]";
repeater.DataPeriods = new List<int>();
// add the repeat block
_datacorder.DataBlocks.Add(repeater);
// now iterate through and add the blocks to be repeated
for (int i = 0; i < blockCnt; i++)
{
var block = _datacorder.DataBlocks[loopStart + i];
_datacorder.DataBlocks.Add(block);
}
}
}
#endregion
#region ID 26 - Call sequence
/* length: [00,01]*02+02
Offset Value Type Description
0x00 N WORD Number of calls to be made
0x02 - WORD[N] Array of call block numbers (relative-signed offsets)
This block is an analogue of the CALL Subroutine statement. It basically executes a sequence of blocks that are somewhere else and
then goes back to the next block. Because more than one call can be normally used you can include a list of sequences to be called.
The 'nesting' of call blocks is also not allowed for the simplicity reasons. You can, of course, use the CALL blocks in the LOOP
sequences and vice versa. The value is relative for the obvious reasons - so that you can add some blocks in the beginning of the
file without disturbing the call values. Please take a look at 'Jump To Block' for reference on the values. */
private void ProcessBlockID26(byte[] data)
{
// block processing not implemented for this - just gets added for informational purposes only
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x26;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Call_Sequence;
int blockSize = 2 + 2 * GetWordValue(data, _position);
// add to tape
_datacorder.DataBlocks.Add(t);
// advance to next block
_position += blockSize;
}
#endregion
#region ID 27 - Return from sequence
/* length: 00
This block indicates the end of the Called Sequence. The next block played will be the block after the last CALL block (or the next Call,
if the Call block had multiple calls).
Again, this block has no body. */
private void ProcessBlockID27(byte[] data)
{
// block processing not implemented for this - just gets added for informational purposes only
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x27;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Return_From_Sequence;
// add to tape
_datacorder.DataBlocks.Add(t);
}
#endregion
#region ID 28 - Select block
/* length: [00,01]+02
Offset Value Type Description
0x00 - WORD Length of the whole block (without these two bytes)
0x02 N BYTE Number of selections
0x03 - SELECT[N] List of selections
----
SELECT structure format
Offset Value Type Description
0x00 - WORD Relative Offset
0x02 L BYTE Length of description text
0x03 - CHAR[L] Description text (please use single line and max. 30 chars)
----
This block is useful when the tape consists of two or more separately-loadable parts. With this block, you are able to select
one of the parts and the utility/emulator will start loading from that block. For example you can use it when the game has a
separate Trainer or when it is a multiload. Of course, to make some use of it the emulator/utility has to show a menu with the
selections when it encounters such a block. All offsets are relative signed words. */
private void ProcessBlockID28(byte[] data)
{
// block processing not implemented for this - just gets added for informational purposes only
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x28;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Select_Block;
int blockSize = 2 + GetWordValue(data, _position);
// add to tape
_datacorder.DataBlocks.Add(t);
// advance to next block
_position += blockSize;
}
#endregion
#region ID 2A - Stop the tape if in 48K mode
/* length: 04
Offset Value Type Description
0x00 0 DWORD Length of the block without these four bytes (0)
When this block is encountered, the tape will stop ONLY if the machine is an 48K Spectrum. This block is to be used for
multiloading games that load one level at a time in 48K mode, but load the entire tape at once if in 128K mode.
This block has no body of its own, but follows the extension rule. */
private void ProcessBlockID2A(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x2A;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Stop_the_Tape_48K;
t.Command = TapeCommand.STOP_THE_TAPE_48K;
int blockSize = 4 + GetWordValue(data, _position);
// add to tape
_datacorder.DataBlocks.Add(t);
// advance to next block
_position += blockSize;
}
#endregion
#region ID 2B - Set signal level
/* length: 05
Offset Value Type Description
0x00 1 DWORD Block length (without these four bytes)
0x04 - BYTE Signal level (0=low, 1=high)
This block sets the current signal level to the specified value (high or low). It should be used whenever it is necessary to avoid any
ambiguities, e.g. with custom loaders which are level-sensitive. */
private void ProcessBlockID2B(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x2B;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Set_Signal_Level;
// add to tape
_datacorder.DataBlocks.Add(t);
// advance to next block
_position += 5;
}
#endregion
#region ID 30 - Text description
/* length: [00]+01
Offset Value Type Description
0x00 N BYTE Length of the text description
0x01 - CHAR[N] Text description in ASCII format
This is meant to identify parts of the tape, so you know where level 1 starts, where to rewind to when the game ends, etc.
This description is not guaranteed to be shown while the tape is playing, but can be read while browsing the tape or changing
the tape pointer.
The description can be up to 255 characters long but please keep it down to about 30 so the programs can show it in one line
(where this is appropriate).
Please use 'Archive Info' block for title, authors, publisher, etc. */
private void ProcessBlockID30(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x30;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Text_Description;
int textLen = data[_position];
_position++;
string desc = Encoding.ASCII.GetString(data, _position, textLen);
//t.BlockDescription = "[" + desc + "]";
// add to tape
_datacorder.DataBlocks.Add(t);
// advance to next block
_position += textLen;
}
#endregion
#region ID 31 - Message block
/* length: [01]+02
Offset Value Type Description
0x00 - BYTE Time (in seconds) for which the message should be displayed
0x01 N BYTE Length of the text message
0x02 - CHAR[N] Message that should be displayed in ASCII format
This will enable the emulators to display a message for a given time. This should not stop the tape and it should not make silence.
If the time is 0 then the emulator should wait for the user to press a key.
The text message should:
stick to a maximum of 30 chars per line;
use single 0x0D (13 decimal) to separate lines;
stick to a maximum of 8 lines.
If you do not obey these rules, emulators may display your message in any way they like. */
private void ProcessBlockID31(byte[] data)
{
// currently not implemented properly in ZXHawk
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x31;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Message_Block;
_position++;
int msgLen = data[_position];
_position++;
string desc = Encoding.ASCII.GetString(data, _position, msgLen);
t.Command = TapeCommand.SHOW_MESSAGE;
//t.BlockDescription = "[MESSAGE: " + desc + "]";
// add to tape
_datacorder.DataBlocks.Add(t);
// advance to next block
_position += msgLen;
}
#endregion
#region ID 32 - Archive info
/* length: [00,01]+02
Offset Value Type Description
0x00 - WORD Length of the whole block (without these two bytes)
0x02 N BYTE Number of text strings
0x03 - TEXT[N] List of text strings
----
TEXT structure format
Offset Value Type Description
0x00 - BYTE Text identification byte:
00 - Full title
01 - Software house/publisher
02 - Author(s)
03 - Year of publication
04 - Language
05 - Game/utility type
06 - Price
07 - Protection scheme/loader
08 - Origin
FF - Comment(s)
0x01 L BYTE Length of text string
0x02 - CHAR[L] Text string in ASCII format
----
Use this block at the beginning of the tape to identify the title of the game, author, publisher, year of publication, price (including
the currency), type of software (arcade adventure, puzzle, word processor, ...), protection scheme it uses (Speedlock 1, Alkatraz, ...)
and its origin (Original, Budget re-release, ...), etc. This block is built in a way that allows easy future expansion.
The block consists of a series of text strings. Each text has its identification number (which tells us what the text means) and then
the ASCII text. To make it possible to skip this block, if needed, the length of the whole block is at the beginning of it.
If all texts on the tape are in English language then you don't have to supply the 'Language' field
The information about what hardware the tape uses is in the 'Hardware Type' block, so no need for it here. */
private void ProcessBlockID32(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x32;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Archive_Info;
int blockLen = GetWordValue(data, 0);
_position += 2;
int stringCount = data[_position++];
// iterate through each string
for (int s = 0; s < stringCount; s++)
{
// identify the type of text
int type = data[_position++];
// get text length
int strLen = data[_position++];
string title = "Info: ";
switch (type)
{
case 0x00:
title = "Full Title: ";
break;
case 0x01:
title = "Software House/Publisher: ";
break;
case 0x02:
title = "Author(s): ";
break;
case 0x03:
title = "Year of Publication: ";
break;
case 0x04:
title = "Language: ";
break;
case 0x05:
title = "Game/Utility Type: ";
break;
case 0x06:
title = "Price: ";
break;
case 0x07:
title = "Protection Scheme/Loader: ";
break;
case 0x08:
title = "Origin: ";
break;
case 0xFF:
title = "Comment(s): ";
break;
default:
break;
}
// add title to description
//t.BlockDescription += title;
// get string data
string val = Encoding.ASCII.GetString(data, _position, strLen);
//t.BlockDescription += val + " \n";
// advance to next string block
_position += strLen;
}
// add to tape
_datacorder.DataBlocks.Add(t);
}
#endregion
#region ID 33 - Hardware type
/* length: [00]*03+01
Offset Value Type Description
0x00 N BYTE Number of machines and hardware types for which info is supplied
0x01 - HWINFO[N] List of machines and hardware
----
HWINFO structure format
Offset Value Type Description
0x00 - BYTE Hardware type
0x01 - BYTE Hardware ID
0x02 - BYTE Hardware information:
00 - The tape RUNS on this machine or with this hardware,
but may or may not use the hardware or special features of the machine.
01 - The tape USES the hardware or special features of the machine,
such as extra memory or a sound chip.
02 - The tape RUNS but it DOESN'T use the hardware
or special features of the machine.
03 - The tape DOESN'T RUN on this machine or with this hardware.
----
This blocks contains information about the hardware that the programs on this tape use. Please include only machines and hardware for
which you are 100% sure that it either runs (or doesn't run) on or with, or you know it uses (or doesn't use) the hardware or special
features of that machine.
If the tape runs only on the ZX81 (and TS1000, etc.) then it clearly won't work on any Spectrum or Spectrum variant, so there's no
need to list this information.
If you are not sure or you haven't tested a tape on some particular machine/hardware combination then do not include it in the list.
The list of hardware types and IDs is somewhat large, and may be found at the end of the format description. */
private void ProcessBlockID33(byte[] data)
{
// currently not implemented properly in ZXHawk
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x33;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Hardware_Type;
_position += 2;
int blockLen = GetWordValue(data, 0);
// add to tape
_datacorder.DataBlocks.Add(t);
// advance to next block
_position += blockLen;
}
#endregion
#region ID 35 - Custom info block
/* length: [10,11,12,13]+14
Offset Value Type Description
0x00 - CHAR[10] Identification string (in ASCII)
0x10 L DWORD Length of the custom info
0x14 - BYTE[L] Custom info
This block can be used to save any information you want. For example, it might contain some information written by a utility,
extra settings required by a particular emulator, or even poke data. */
private void ProcessBlockID35(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x35;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Custom_Info_Block;
string info = Encoding.ASCII.GetString(data, _position, 0x10);
//t.BlockDescription = "[CUSTOM INFO: " + info + "]";
_position += 0x10;
int blockLen = BitConverter.ToInt32(data, _position);
_position += 4;
// add to tape
_datacorder.DataBlocks.Add(t);
// advance to next block
_position += blockLen;
}
#endregion
#region ID 5A - "Glue" block
/* length: 09
Offset Value Type Description
0x00 - BYTE[9] Value: { "XTape!",0x1A,MajR,MinR }
Just skip these 9 bytes and you will end up on the next ID.
This block is generated when you merge two ZX Tape files together. It is here so that you can easily copy the files together and use
them. Of course, this means that resulting file would be 10 bytes longer than if this block was not used. All you have to do
if you encounter this block ID is to skip next 9 bytes.
If you can avoid using this block for this purpose, then do so; it is preferable to use a utility to join the two files and
ensure that they are both of the higher version number. */
private void ProcessBlockID5A(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x5A;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Glue_Block;
// add to tape
_datacorder.DataBlocks.Add(t);
// advance to next block
_position += 9;
}
#endregion
#region UnDetected Blocks
private void ProcessUnidentifiedBlock(byte[] data)
{
TapeDataBlock t = new TapeDataBlock();
t.BlockID = -2;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Unsupported;
//t.BlockDescription = "[UNSUPPORTED - 0x" + data[_position - 1] + "]";
_position += GetInt32(data, _position) & 0xFFFFFF;
// add to tape
_datacorder.DataBlocks.Add(t);
// advance to next block
_position += 4;
}
#endregion
#region Depreciated Blocks
// These mostly should be ignored by ZXHawk - here for completeness
#region ID 16 - C64 ROM Type Data Block
private void ProcessBlockID16(byte[] data)
{
}
#endregion
#region ID 17 - C64 Turbo Tape Data Block
private void ProcessBlockID17(byte[] data)
{
}
#endregion
#region ID 34 - Emulation info
private void ProcessBlockID34(byte[] data)
{
// currently not implemented properly in ZXHawk
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x34;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Emulation_Info;
// add to tape
_datacorder.DataBlocks.Add(t);
// advance to next block
_position += 8;
}
#endregion
#region ID 40 - Snapshot block
/* length: [01,02,03]+04
Offset Value Type Description
0x00 - BYTE Snapshot type:
00: .Z80 format
01: .SNA format
0x01 L BYTE[3] Snapshot length
0x04 - BYTE[L] Snapshot itself
This would enable one to snapshot the game at the start and still have all the tape blocks (level data, etc.) in the same file.
Only .Z80 and .SNA snapshots are supported for compatibility reasons!
The emulator should take care of that the snapshot is not taken while the actual Tape loading is taking place (which doesn't do much sense).
And when an emulator encounters the snapshot block it should load it and then continue with the next block. */
private void ProcessBlockID40(byte[] data)
{
// currently not implemented properly in ZXHawk
TapeDataBlock t = new TapeDataBlock();
t.BlockID = 0x40;
t.DataPeriods = new List<int>();
t.BlockDescription = BlockType.Snapshot_Block;
_position++;
int blockLen = data[_position] |
data[_position + 1] << 8 |
data[_position + 2] << 16;
_position += 3;
// add to tape
_datacorder.DataBlocks.Add(t);
// advance to next block
_position += blockLen;
}
#endregion
#endregion
#endregion
#region DataBlockDecoder
/// <summary>
/// Used to process either a standard or turbo data block
/// </summary>
/// <param name="block"></param>
/// <param name="blockData"></param>
/// <returns></returns>
private TapeDataBlock DecodeDataBlock
(
TapeDataBlock block,
byte[] blockdata,
DataBlockType dataBlockType,
int pauseAfterBlock,
int pilotCount,
int pilotToneLength = 2168,
int sync1PulseLength = 667,
int sync2PulseLength = 735,
int bit0PulseLength = 855,
int bit1PulseLength = 1710,
int bitsInLastByte = 8
)
{
// first get the block description
string description = string.Empty;
// process the type byte
/* (The type is 0,1,2 or 3 for a Program, Number array, Character array or Code file.
A SCREEN$ file is regarded as a Code file with start address 16384 and length 6912 decimal.
If the file is a Program file, parameter 1 holds the autostart line number (or a number >=32768 if no LINE parameter was given)
and parameter 2 holds the start of the variable area relative to the start of the program. If it's a Code file, parameter 1 holds
the start of the code block when saved, and parameter 2 holds 32768. For data files finally, the byte at position 14 decimal holds the variable name.)
*/
int blockSize = blockdata.Length;
// dont get description info for Pure Data Blocks
if (dataBlockType != DataBlockType.Pure)
{
if (blockdata[0] == 0x00 && blockSize == 19 && (blockdata[1] == 0x00) || blockdata[1] == 3)
{
// This is the program header
string fileName = Encoding.ASCII.GetString(blockdata.Skip(2).Take(10).ToArray()).Trim();
string type = "";
if (blockdata[0] == 0x00)
{
type = "Program";
block.AddMetaData(BlockDescriptorTitle.Program, fileName);
}
else
{
type = "Bytes";
block.AddMetaData(BlockDescriptorTitle.Bytes, fileName);
}
// now build the description string
StringBuilder sb = new StringBuilder();
sb.Append(type + ": ");
sb.Append(fileName + " ");
sb.Append(GetWordValue(blockdata, 14));
sb.Append(":");
sb.Append(GetWordValue(blockdata, 12));
description = sb.ToString();
}
else if (blockdata[0] == 0xFF)
{
// this is a data block
description = "Data Block " + (blockSize - 2) + "bytes";
block.AddMetaData(BlockDescriptorTitle.Data_Bytes, (blockSize - 2).ToString() + " Bytes");
}
else
{
// other type
description = string.Format("#{0} block, {1} bytes", blockdata[0].ToString("X2"), blockSize - 2);
//description += string.Format(", crc {0}", ((crc != 0) ? string.Format("bad (#{0:X2}!=#{1:X2})", crcFile, crcValue) : "ok"));
block.AddMetaData(BlockDescriptorTitle.Undefined, description);
}
}
// update metadata
switch (dataBlockType)
{
case DataBlockType.Standard:
case DataBlockType.Turbo:
if (dataBlockType == DataBlockType.Standard)
block.BlockDescription = BlockType.Standard_Speed_Data_Block;
if (dataBlockType == DataBlockType.Turbo)
block.BlockDescription = BlockType.Turbo_Speed_Data_Block;
block.AddMetaData(BlockDescriptorTitle.Pilot_Pulse_Length, pilotToneLength.ToString() + " T-States");
block.AddMetaData(BlockDescriptorTitle.Pilot_Pulse_Count, pilotCount.ToString() + " Pulses");
block.AddMetaData(BlockDescriptorTitle.First_Sync_Length, sync1PulseLength.ToString() + " T-States");
block.AddMetaData(BlockDescriptorTitle.Second_Sync_Length, sync2PulseLength.ToString() + " T-States");
break;
case DataBlockType.Pure:
block.BlockDescription = BlockType.Pure_Data_Block;
break;
}
block.AddMetaData(BlockDescriptorTitle.Zero_Bit_Length, bit0PulseLength.ToString() + " T-States");
block.AddMetaData(BlockDescriptorTitle.One_Bit_Length, bit1PulseLength.ToString() + " T-States");
block.AddMetaData(BlockDescriptorTitle.Data_Length, blockSize.ToString() + " Bytes");
block.AddMetaData(BlockDescriptorTitle.Bits_In_Last_Byte, bitsInLastByte.ToString() + " Bits");
block.AddMetaData(BlockDescriptorTitle.Pause_After_Data, pauseAfterBlock.ToString() + " ms");
// calculate period information
List <int> dataPeriods = new List<int>();
// generate pilot pulses
if (pilotCount > 0)
{
for (int i = 0; i < pilotCount; i++)
{
dataPeriods.Add(pilotToneLength);
}
// add syncro pulses
dataPeriods.Add(sync1PulseLength);
dataPeriods.Add(sync2PulseLength);
}
int pos = 0;
// add bit0 and bit1 periods
for (int i = 0; i < blockSize - 1; i++, pos++)
{
for (byte b = 0x80; b != 0; b >>= 1)
{
if ((blockdata[i] & b) != 0)
dataPeriods.Add(bit1PulseLength);
else
dataPeriods.Add(bit0PulseLength);
if ((blockdata[i] & b) != 0)
dataPeriods.Add(bit1PulseLength);
else
dataPeriods.Add(bit0PulseLength);
}
}
// add the last byte
for (byte c = 0x80; c != (byte)(0x80 >> bitsInLastByte); c >>= 1)
{
if ((blockdata[pos] & c) != 0)
dataPeriods.Add(bit1PulseLength);
else
dataPeriods.Add(bit0PulseLength);
if ((blockdata[pos] & c) != 0)
dataPeriods.Add(bit1PulseLength);
else
dataPeriods.Add(bit0PulseLength);
}
// add block pause if pause is not 0
if (pauseAfterBlock != 0)
{
int actualPause = pauseAfterBlock * 3500;
dataPeriods.Add(actualPause);
}
// add to the tapedatablock object
block.DataPeriods = dataPeriods;
// add the raw data
block.BlockData = blockdata;
return block;
}
/// <summary>
/// Used to process either a standard or turbo data block
/// </summary>
/// <param name="block"></param>
/// <param name="blockData"></param>
/// <returns></returns>
private TapeDataBlock DecodeDataBlock
(
TapeDataBlock block,
byte[] blockData,
DataBlockType dataBlockType,
int pauseAfterBlock,
int pilotToneLength = 2168,
int sync1PulseLength = 667,
int sync2PulseLength = 735,
int bit0PulseLength = 855,
int bit1PulseLength = 1710,
int bitsInLastByte = 8
)
{
// pilot count needs to be ascertained from flag byte
int pilotCount;
if (blockData[0] < 128)
pilotCount = 8063;
else
pilotCount = 3223;
// now we can decode
var nBlock = DecodeDataBlock
(
block,
blockData,
dataBlockType,
pauseAfterBlock,
pilotCount,
pilotToneLength,
sync1PulseLength,
sync2PulseLength,
bit0PulseLength,
bit1PulseLength,
bitsInLastByte
);
return nBlock;
}
#endregion
}
public enum DataBlockType
{
Standard,
Turbo,
Pure
}
}