ZXHawk: Added Compressed Square Wave (CSW) tape image support

This commit is contained in:
Asnivor 2018-06-22 13:23:33 +01:00
parent b90c8f0bec
commit b81a7539cf
12 changed files with 372 additions and 7 deletions

View File

@ -66,7 +66,7 @@ namespace BizHawk.Client.Common
{
RomData = FileData;
}
else if (file.Extension == ".DSK" || file.Extension == ".TAP" || file.Extension == ".TZX" || file.Extension == ".PZX")
else if (file.Extension == ".DSK" || file.Extension == ".TAP" || file.Extension == ".TZX" || file.Extension == ".PZX" || file.Extension == ".CSW")
{
// 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", ".PZX"
".EXE", ".PRG", ".D64", "*G64", ".CRT", ".TAP", ".32X", ".MDS", ".TZX", ".PZX", ".CSW"
};
}

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;*.pzx;%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;*.csw;%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;*.pzx;%ARCH%",
"Sinclair ZX Spectrum", "*.tzx;*.tap;*.dsk;*.pzx;*.csw;%ARCH%",
"All Files", "*.*");
}

View File

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

View File

@ -293,6 +293,7 @@
<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\CSW\CswConverter.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" />

View File

@ -324,6 +324,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
TzxConverter tzxSer = new TzxConverter(this);
TapConverter tapSer = new TapConverter(this);
PzxConverter pzxSer = new PzxConverter(this);
CswConverter cswSer = new CswConverter(this);
// TZX
if (tzxSer.CheckType(tapeData))
@ -365,6 +366,26 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
}
}
// CSW
else if (cswSer.CheckType(tapeData))
{
// this file has a csw header - attempt serialization
try
{
cswSer.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 CSW header, but threw an exception whilst data was being parsed.\n\n" + e.ToString());
}
}
// Assume TAP
else
{
@ -817,7 +838,12 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
if (_tapeIsPlaying && _autoPlay)
{
_monitorTimeOut--;
if (DataBlocks.Count > 1 || _dataBlocks[_currentDataBlockIndex].BlockDescription != BlockType.CSW_Recording)
{
// we should only stop the tape when there are multiple blocks
// if we just have one big block (maybe a CSW or WAV) then auto stopping will cock things up
_monitorTimeOut--;
}
if (_monitorTimeOut < 0)
{
@ -843,6 +869,10 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
if (timeout == 0)
return;
// dont autostop if there is only 1 block
if (DataBlocks.Count > 2 || _dataBlocks[_currentDataBlockIndex].BlockDescription == BlockType.CSW_Recording)
return;
if (diff >= timeout * 2)
{
// There have been no attempted tape reads by the CPU within the double timeout period

View File

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

View File

@ -1,4 +1,6 @@
using System;
using System.IO;
using System.IO.Compression;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
@ -130,6 +132,22 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
return res;
}
/// <summary>
/// Decompresses a byte array that is Z-RLE compressed
/// </summary>
/// <param name="sourceBuffer"></param>
/// <param name="destBuffer"></param>
public static void DecompressZRLE(byte[] sourceBuffer, ref byte[] destBuffer)
{
MemoryStream stream = new MemoryStream();
stream.Write(sourceBuffer, 0, sourceBuffer.Length);
stream.Position = 0;
stream.ReadByte();
stream.ReadByte();
DeflateStream ds = new DeflateStream(stream, CompressionMode.Decompress, false);
ds.Read(destBuffer, 0, destBuffer.Length);
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,268 @@
using BizHawk.Common.NumberExtensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// Responsible for Compressed Square Wave conversion
/// https://web.archive.org/web/20171024182530/http://ramsoft.bbk.org.omegahg.com/csw.html
/// </summary>
public class CswConverter : MediaConverter
{
/// <summary>
/// The type of serializer
/// </summary>
private MediaConverterType _formatType = MediaConverterType.CSW;
public override MediaConverterType FormatType
{
get
{
return _formatType;
}
}
/// <summary>
/// Position counter
/// </summary>
private int _position = 0;
/// <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; } }
#region Construction
private DatacorderDevice _datacorder;
public CswConverter(DatacorderDevice _tapeDevice)
{
_datacorder = _tapeDevice;
}
#endregion
/// <summary>
/// Returns TRUE if pzx header is detected
/// </summary>
/// <param name="data"></param>
public override bool CheckType(byte[] data)
{
// CSW Header
// check whether this is a valid csw format file by looking at the identifier in the header
// (first 22 bytes of the file)
string ident = Encoding.ASCII.GetString(data, 0, 22);
// version info
int majorVer = data[8];
int minorVer = data[9];
if (ident.ToUpper() != "COMPRESSED SQUARE WAVE")
{
// this is not a valid CSW 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();
// CSW Header
// check whether this is a valid csw format file by looking at the identifier in the header
// (first 22 bytes of the file)
string ident = Encoding.ASCII.GetString(data, 0, 22);
if (ident.ToUpper() != "COMPRESSED SQUARE WAVE")
{
// this is not a valid CSW format file
throw new Exception(this.GetType().ToString() +
"This is not a valid CSW format file");
}
if (data[0x16] != 0x1a)
{
// invalid terminator code
throw new Exception(this.GetType().ToString() +
"This image reports as a CSW but has an invalid terminator code");
}
_position = 0;
// version info
int majorVer = data[0x17];
int minorVer = data[0x18];
int sampleRate;
int totalPulses;
byte compressionType;
byte flags;
byte headerExtensionLen;
byte[] cswData;
byte[] cswDataUncompressed;
if (majorVer == 2)
{
/*
CSW-2 Header
CSW global file header - status: required
Offset Value Type Description
0x00 (note) ASCII[22] "Compressed Square Wave" signature
0x16 0x1A BYTE Terminator code
0x17 0x02 BYTE CSW major revision number
0x18 0x00 BYTE CSW minor revision number
0x19 - DWORD Sample rate
0x1D - DWORD Total number of pulses (after decompression)
0x21 - BYTE Compression type (see notes below)
0x01: RLE
0x02: Z-RLE
0x22 - BYTE Flags
b0: initial polarity; if set, the signal starts at logical high
0x23 HDR BYTE Header extension length in bytes (0x00)
For future expansions only, see note below.
0x24 - ASCIIZ[16] Encoding application description
Information about the tool which created the file (e.g. name and version)
0x34 - BYTE[HDR] Header extension data (if present)
0x34+HDR - - CSW data.
*/
_position = 0x19;
sampleRate = GetInt32(data, _position);
_position += 4;
totalPulses = GetInt32(data, _position);
cswDataUncompressed = new byte[totalPulses + 1];
_position += 4;
compressionType = data[_position++];
flags = data[_position++];
headerExtensionLen = data[_position++];
_position = 0x34 + headerExtensionLen;
cswData = new byte[data.Length - _position];
Array.Copy(data, _position, cswData, 0, cswData.Length);
ProcessCSWV2(cswData, ref cswDataUncompressed, compressionType, totalPulses);
}
else if (majorVer == 1)
{
/*
CSW-1 Header
CSW global file header - status: required
Offset Value Type Description
0x00 (note) ASCII[22] "Compressed Square Wave" signature
0x16 0x1A BYTE Terminator code
0x17 0x01 BYTE CSW major revision number
0x18 0x01 BYTE CSW minor revision number
0x19 - WORD Sample rate
0x1B 0x01 BYTE Compression type
0x01: RLE
0x1C - BYTE Flags
b0: initial polarity; if set, the signal starts at logical high
0x1D 0x00 BYTE[3] Reserved.
0x20 - - CSW data.
*/
_position = 0x19;
sampleRate = GetWordValue(data, _position);
_position += 2;
compressionType = data[_position++];
flags = data[_position++];
_position += 3;
cswDataUncompressed = new byte[data.Length - _position];
if (compressionType == 1)
Array.Copy(data, _position, cswDataUncompressed, 0, cswDataUncompressed.Length);
else
throw new Exception(this.GetType().ToString() +
"CSW Format unknown compression type");
}
else
{
throw new Exception(this.GetType().ToString() +
"CSW Format Version " + majorVer + "." + minorVer + " is not currently supported");
}
// create the single tape block
// (use DATA block for now so initial signal level is handled correctly by the datacorder device)
TapeDataBlock t = new TapeDataBlock();
t.BlockDescription = BlockType.CSW_Recording;
t.BlockID = 0x18;
t.DataPeriods = new List<int>();
if (flags.Bit(0))
t.InitialPulseLevel = true;
else
t.InitialPulseLevel = false;
var rate = (69888 * 50) / sampleRate;
for (int i = 0; i < cswDataUncompressed.Length;)
{
int length = cswDataUncompressed[i++] * rate;
if (length == 0)
{
length = GetInt32(cswDataUncompressed, i) / rate;
i += 4;
}
t.DataPeriods.Add(length);
}
// add closing period
t.DataPeriods.Add((69888 * 50) / 10);
// add to datacorder
_datacorder.DataBlocks.Add(t);
}
/// <summary>
/// Processes a CSW v2 data block
/// </summary>
/// <param name="srcBuff"></param>
/// <param name="destBuff"></param>
/// <param name="sampleRate"></param>
/// <param name="compType"></param>
/// <param name="pulseCount"></param>
public static void ProcessCSWV2(
byte[] srcBuff,
ref byte[] destBuff,
byte compType,
int pulseCount)
{
if (compType == 1)
{
Array.Copy(srcBuff, 0, destBuff, 0, pulseCount);
}
else if (compType == 2)
{
DecompressZRLE(srcBuff, ref destBuff);
}
else
throw new Exception("CSW Format unknown compression type");
}
}
}

View File

@ -61,7 +61,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
#endregion
/// <summary>
/// Returns TRUE if tzx header is detected
/// Returns TRUE if pzx header is detected
/// </summary>
/// <param name="data"></param>
public override bool CheckType(byte[] data)

View File

@ -643,10 +643,51 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
int blockLen = GetInt32(data, _position);
_position += 4;
_position += blockLen;
t.PauseInMS = GetWordValue(data, _position);
_position += 2;
int sampleRate = data[_position++] << 16 | data[_position++] << 8 | data[_position++];
byte compType = data[_position++];
int pulses = GetInt32(data, _position);
_position += 4;
int dataLen = blockLen - 10;
// build source array
byte[] src = new byte[dataLen];
// build destination array
byte[] dest = new byte[pulses + 1];
// process the CSW data
CswConverter.ProcessCSWV2(src, ref dest, compType, pulses);
// create the periods
var rate = (69888 * 50) / sampleRate;
for (int i = 0; i < dest.Length;)
{
int length = dest[i++] * rate;
if (length == 0)
{
length = GetInt32(dest, i) / rate;
i += 4;
}
t.DataPeriods.Add(length);
}
// add closing period
t.DataPeriods.Add((69888 * 50) / 10);
_position += dataLen;
//_position += blockLen;
// add the block
_datacorder.DataBlocks.Add(t);
// generate PAUSE block
CreatePauseBlock(_datacorder.DataBlocks.Last());
}
#endregion