ZXHawk: Experimental PZX tape image support

This commit is contained in:
Asnivor 2018-06-20 15:03:11 +01:00
parent 5b0a41e31c
commit 50123bf8e2
12 changed files with 461 additions and 11 deletions

View File

@ -66,7 +66,7 @@ namespace BizHawk.Client.Common
{
RomData = FileData;
}
else if (file.Extension == ".DSK" || file.Extension == ".TAP" || file.Extension == ".TZX")
else if (file.Extension == ".DSK" || file.Extension == ".TAP" || file.Extension == ".TZX" || file.Extension == ".PZX")
{
// these are not roms. unforunately if treated as such there are certain edge-cases
// where a header offset is detected. This should mitigate this issue until a cleaner solution is found

View File

@ -51,7 +51,7 @@ namespace BizHawk.Client.EmuHawk
return new[]
{
".NES", ".FDS", ".UNF", ".SMS", ".GG", ".SG", ".GB", ".GBC", ".GBA", ".PCE", ".SGX", ".BIN", ".SMD", ".GEN", ".MD", ".SMC", ".SFC", ".A26", ".A78", ".LNX", ".COL", ".ROM", ".M3U", ".CUE", ".CCD", ".SGB", ".Z64", ".V64", ".N64", ".WS", ".WSC", ".XML", ".DSK", ".DO", ".PO", ".PSF", ".MINIPSF", ".NSF",
".EXE", ".PRG", ".D64", "*G64", ".CRT", ".TAP", ".32X", ".MDS", ".TZX"
".EXE", ".PRG", ".D64", "*G64", ".CRT", ".TAP", ".32X", ".MDS", ".TZX", ".PZX"
};
}

View File

@ -2081,7 +2081,7 @@ namespace BizHawk.Client.EmuHawk
if (VersionInfo.DeveloperBuild)
{
return FormatFilter(
"Rom Files", "*.nes;*.fds;*.unf;*.sms;*.gg;*.sg;*.pce;*.sgx;*.bin;*.smd;*.rom;*.a26;*.a78;*.lnx;*.m3u;*.cue;*.ccd;*.mds;*.exe;*.gb;*.gbc;*.gba;*.gen;*.md;*.32x;*.col;*.int;*.smc;*.sfc;*.prg;*.d64;*.g64;*.crt;*.tap;*.sgb;*.xml;*.z64;*.v64;*.n64;*.ws;*.wsc;*.dsk;*.do;*.po;*.vb;*.ngp;*.ngc;*.psf;*.minipsf;*.nsf;*.tzx;%ARCH%",
"Rom Files", "*.nes;*.fds;*.unf;*.sms;*.gg;*.sg;*.pce;*.sgx;*.bin;*.smd;*.rom;*.a26;*.a78;*.lnx;*.m3u;*.cue;*.ccd;*.mds;*.exe;*.gb;*.gbc;*.gba;*.gen;*.md;*.32x;*.col;*.int;*.smc;*.sfc;*.prg;*.d64;*.g64;*.crt;*.tap;*.sgb;*.xml;*.z64;*.v64;*.n64;*.ws;*.wsc;*.dsk;*.do;*.po;*.vb;*.ngp;*.ngc;*.psf;*.minipsf;*.nsf;*.tzx;*.pzx;%ARCH%",
"Music Files", "*.psf;*.minipsf;*.sid;*.nsf",
"Disc Images", "*.cue;*.ccd;*.mds;*.m3u",
"NES", "*.nes;*.fds;*.unf;*.nsf;%ARCH%",
@ -2109,7 +2109,7 @@ namespace BizHawk.Client.EmuHawk
"Apple II", "*.dsk;*.do;*.po;%ARCH%",
"Virtual Boy", "*.vb;%ARCH%",
"Neo Geo Pocket", "*.ngp;*.ngc;%ARCH%",
"Sinclair ZX Spectrum", "*.tzx;*.tap;*.dsk;%ARCH%",
"Sinclair ZX Spectrum", "*.tzx;*.tap;*.dsk;*.pzx;%ARCH%",
"All Files", "*.*");
}

View File

@ -304,6 +304,7 @@ namespace BizHawk.Emulation.Common
break;
case ".TZX":
case ".PZX":
game.System = "ZXSpectrum";
break;

View File

@ -293,10 +293,11 @@
<Compile Include="Computers\SinclairSpectrum\Media\Disk\DiskType.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\MediaConverter.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\MediaConverterType.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\PZX\PzxConverter.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TapeCommand.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TapeDataBlock.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TapConverter.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TzxConverter.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TAP\TapConverter.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\TZX\TzxConverter.cs" />
<Compile Include="Computers\SinclairSpectrum\SoundProviderMixer.cs" />
<Compile Include="Computers\SinclairSpectrum\Machine\MachineType.cs" />
<Compile Include="Computers\SinclairSpectrum\Machine\SpectrumBase.cs" />

View File

@ -320,8 +320,12 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
/// <param name="tapeData"></param>
public void LoadTape(byte[] tapeData)
{
// check TZX first
// instantiate converters
TzxConverter tzxSer = new TzxConverter(this);
TapConverter tapSer = new TapConverter(this);
PzxConverter pzxSer = new PzxConverter(this);
// TZX
if (tzxSer.CheckType(tapeData))
{
// this file has a tzx header - attempt serialization
@ -340,9 +344,30 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
"\n\nTape image file has a valid TZX header, but threw an exception whilst data was being parsed.\n\n" + e.ToString());
}
}
// PZX
else if (pzxSer.CheckType(tapeData))
{
// this file has a pzx header - attempt serialization
try
{
pzxSer.Read(tapeData);
// reset block index
CurrentDataBlockIndex = 0;
return;
}
catch (Exception ex)
{
// exception during operation
var e = ex;
throw new Exception(this.GetType().ToString() +
"\n\nTape image file has a valid PZX header, but threw an exception whilst data was being parsed.\n\n" + e.ToString());
}
}
// Assume TAP
else
{
TapConverter tapSer = new TapConverter(this);
try
{
tapSer.Read(tapeData);
@ -383,7 +408,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
counter++;
if (counter > 50)
if (counter > 30)
{
counter = 0;
bool state = GetEarBit(_machine.CPU.TotalExecutedCycles);
@ -421,7 +446,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
}
// process the cycles based on the waitEdge
while (cycles >= _waitEdge)
while (cycles >= _waitEdge)
{
// decrement cycles
cycles -= _waitEdge;
@ -433,6 +458,9 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
// start of block
//if (!_dataBlocks[_currentDataBlockIndex].InitialPulseLevel[_position])
//currentState = !currentState;
// notify about the current block
var bl = _dataBlocks[_currentDataBlockIndex];

View File

@ -197,6 +197,11 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
// spectrum .tzx tape file
return SpectrumMediaType.Tape;
}
if (hdr.ToUpper().StartsWith("PZXT"))
{
// spectrum .tzx tape file
return SpectrumMediaType.Tape;
}
// if we get this far, assume a .tap file
return SpectrumMediaType.Tape;

View File

@ -9,6 +9,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
NONE,
TZX,
TAP,
PZX,
DSK
}
}

View File

@ -0,0 +1,403 @@
using BizHawk.Common.NumberExtensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// Reponsible for PZX format serializaton
/// Based on the information here: http://zxds.raxoft.cz/docs/pzx.txt
/// </summary>
public class PzxConverter : MediaConverter
{
/// <summary>
/// The type of serializer
/// </summary>
private MediaConverterType _formatType = MediaConverterType.PZX;
public override MediaConverterType FormatType
{
get
{
return _formatType;
}
}
/// <summary>
/// Signs whether this class can be used to read the data format
/// </summary>
public override bool IsReader { get { return true; } }
/// <summary>
/// Signs whether this class can be used to write the data format
/// </summary>
public override bool IsWriter { get { return false; } }
/// <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 PzxConverter(DatacorderDevice _tapeDevice)
{
_datacorder = _tapeDevice;
}
#endregion
/// <summary>
/// Returns TRUE if tzx header is detected
/// </summary>
/// <param name="data"></param>
public override bool CheckType(byte[] data)
{
// PZX Header
// check whether this is a valid pzx format file by looking at the identifier in the header
// (first 4 bytes of the file)
string ident = Encoding.ASCII.GetString(data, 0, 4);
// version info
int majorVer = data[8];
int minorVer = data[9];
if (ident.ToUpper() != "PZXT")
{
// this is not a valid PZX format file
return false;
}
else
{
return true;
}
}
/// <summary>
/// DeSerialization method
/// </summary>
/// <param name="data"></param>
public override void Read(byte[] data)
{
// clear existing tape blocks
_datacorder.DataBlocks.Clear();
/*
// PZX uniform block layout
offset type name meaning
------ ---- ---- -------
0 u32 tag unique identifier for the block type.
4 u32 size size of the block in bytes, excluding the tag and size fields themselves.
8 u8[size] data arbitrary amount of block data.
*/
// check whether this is a valid pzx format file by looking at the identifier in the header block
string ident = Encoding.ASCII.GetString(data, 0, 4);
if (ident.ToUpper() != "PZXT")
{
// this is not a valid TZX format file
throw new Exception(this.GetType().ToString() +
"This is not a valid PZX format file");
}
_position = 0;
// parse all blocks out into seperate byte arrays first
List<byte[]> bDatas = new List<byte[]>();
while (_position < data.Length)
{
int startPos = _position;
// data size
_position += 4;
int blockSize = GetInt32(data, _position);
_position += 4;
// block data
byte[] bd = new byte[8 + blockSize];
Array.Copy(data, startPos, bd, 0, bd.Length);
bDatas.Add(bd);
_position += blockSize;
}
// process the blocks
foreach (var b in bDatas)
{
int pos = 8;
string blockId = Encoding.ASCII.GetString(b, 0, 4);
int blockSize = GetInt32(b, 4);
TapeDataBlock t = new TapeDataBlock();
switch (blockId)
{
// PZXT - PZX header block
/*
offset type name meaning
0 u8 major major version number (currently 1).
1 u8 minor minor version number (currently 0).
2 u8[?] info tape info, see below.
*/
case "PZXT":
break;
// PULS - Pulse sequence
/*
offset type name meaning
0 u16 count bits 0-14 optional repeat count (see bit 15), always greater than zero
bit 15 repeat count present: 0 not present 1 present
2 u16 duration1 bits 0-14 low/high (see bit 15) pulse duration bits
bit 15 duration encoding: 0 duration1 1 ((duration1<<16)+duration2)
4 u16 duration2 optional low bits of pulse duration (see bit 15 of duration1)
6 ... ... ditto repeated until the end of the block
*/
case "PULS":
t.BlockID = GetInt32(b, 0);
t.DataPeriods = new List<int>();
List<ushort[]> pulses = new List<ushort[]>();
while (pos < blockSize + 8)
{
ushort[] p = new ushort[2];
p[0] = 1;
p[1] = GetWordValue(b, pos);
pos += 2;
if (p[1] > 0x8000)
{
p[0] = (ushort)(p[1] & 0x7fff);
p[1] = GetWordValue(b, pos);
pos += 2;
}
if (p[1] >= 0x8000)
{
p[1] &= 0x7fff;
p[1] <<= 16;
p[1] |= GetWordValue(b, pos);
pos += 2;
}
pulses.Add(p);
}
// convert to tape block
t.BlockDescription = BlockType.PULS;
t.PauseInMS = 0;
foreach (var x in pulses)
{
for (int i = 0; i < x[0]; i++)
{
t.DataPeriods.Add(x[1]);
t.InitialPulseLevel.Add(true);
}
}
_datacorder.DataBlocks.Add(t);
break;
// DATA - Data block
/*
offset type name meaning
0 u32 count bits 0-30 number of bits in the data stream
bit 31 initial pulse level: 0 low 1 high
4 u16 tail duration of extra pulse after last bit of the block
6 u8 p0 number of pulses encoding bit equal to 0.
7 u8 p1 number of pulses encoding bit equal to 1.
8 u16[p0] s0 sequence of pulse durations encoding bit equal to 0.
8+2*p0 u16[p1] s1 sequence of pulse durations encoding bit equal to 1.
8+2*(p0+p1) u8[ceil(bits/8)] data data stream, see below.
*/
case "DATA":
t.BlockID = GetInt32(b, 0);
t.DataPeriods = new List<int>();
List<ushort> s0 = new List<ushort>();
List<ushort> s1 = new List<ushort>();
List<byte> dData = new List<byte>();
uint initPulseLevel = 1;
int dCount = 1;
ushort tail = 0;
while (pos < blockSize + 8)
{
dCount = GetInt32(b, pos);
initPulseLevel = (uint)((dCount & 0x80000000) == 0 ? 0 : 1);
dCount = (int)(dCount & 0x7FFFFFFF);
pos += 4;
tail = GetWordValue(b, pos);
pos += 2;
var p0 = b[pos++];
var p1 = b[pos++];
for (int i = 0; i < p1; i++)
{
var s = GetWordValue(b, pos);
pos += 2;
s0.Add(s);
}
for (int i = 0; i < p1; i++)
{
var s = GetWordValue(b, pos);
pos += 2;
s1.Add(s);
}
for (int i = 0; i < Math.Ceiling((decimal)dCount / 8); i++)
{
var buff = b[pos++];
dData.Add(buff);
}
bool initPulse = initPulseLevel == 1 ? true : false;
foreach (var by in dData)
{
for (int i = 7; i >= 0; i--)
{
if (by.Bit(i) == true)
{
foreach (var pu in s1)
{
t.DataPeriods.Add((int)pu);
t.InitialPulseLevel.Add(initPulse);
}
}
else
{
foreach (var pu in s0)
{
t.DataPeriods.Add((int)pu);
t.InitialPulseLevel.Add(initPulse);
}
}
}
}
dData.Clear();
}
// convert to tape block
t.BlockDescription = BlockType.DATA;
t.PauseInMS = 0;
// tail
t.DataPeriods.Add(tail);
_datacorder.DataBlocks.Add(t);
break;
// PAUS - Pause
/*
offset type name meaning
0 u32 duration bits 0-30 duration of the pause
bit 31 initial pulse level: 0 low 1 high
*/
case "PAUS":
t.BlockID = GetInt32(b, 0);
t.DataPeriods = new List<int>();
int iniPulseLevel = 1;
int pCount = 0;
var d = GetInt32(b, pos);
iniPulseLevel = ((d & 0x80000000) == 0 ? 0 : 1);
pCount = (d & 0x7FFFFFFF);
// convert to tape block
t.BlockDescription = BlockType.PAUS;
t.DataPeriods.Add(pCount);
if (iniPulseLevel == 1)
t.InitialPulseLevel.Add(true);
else
t.InitialPulseLevel.Add(false);
_datacorder.DataBlocks.Add(t);
break;
// BRWS - Browse point
/*
offset type name meaning
0 u8[?] text text describing this browse point
*/
case "BRWS":
t.BlockID = GetInt32(b, 0);
t.DataPeriods = new List<int>();
string info = Encoding.ASCII.GetString(b, 8, blockSize);
// convert to tape block
t.BlockDescription = BlockType.BRWS;
t.MetaData.Add(BlockDescriptorTitle.Comments, info);
t.PauseInMS = 0;
_datacorder.DataBlocks.Add(t);
break;
// STOP - Stop tape command
/*
offset type name meaning
0 u16 flags when exactly to stop the tape (1 48k only, other always).
*/
case "STOP":
t.BlockID = GetInt32(b, 0);
t.DataPeriods = new List<int>();
var flags = GetWordValue(b, pos);
if (flags == 1)
{
t.BlockDescription = BlockType.Stop_the_Tape_48K;
t.Command = TapeCommand.STOP_THE_TAPE_48K;
}
else
{
t.BlockDescription = BlockType.Pause_or_Stop_the_Tape;
t.Command = TapeCommand.STOP_THE_TAPE;
}
_datacorder.DataBlocks.Add(t);
break;
}
}
}
}
}

View File

@ -113,6 +113,8 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
/// </summary>
public List<int> DataPeriods = new List<int>();
public List<bool> InitialPulseLevel = new List<bool>();
/// <summary>
/// Command that is raised by this data block
/// (that may or may not need to be acted on)
@ -229,8 +231,17 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
Snapshot_Block = 0x40,
// unsupported / undetected
Unsupported
Unsupported,
// PZX blocks
PZXT,
PULS,
DATA,
BRWS,
PAUS
}
/// <summary>
/// Different title possibilities