From b81a7539cfbf7bf58c4dbe543299083bfdc333cf Mon Sep 17 00:00:00 2001 From: Asnivor Date: Fri, 22 Jun 2018 13:23:33 +0100 Subject: [PATCH] ZXHawk: Added Compressed Square Wave (CSW) tape image support --- BizHawk.Client.Common/RomGame.cs | 2 +- BizHawk.Client.EmuHawk/FileLoader.cs | 2 +- BizHawk.Client.EmuHawk/MainForm.cs | 4 +- BizHawk.Emulation.Common/Database/Database.cs | 1 + .../BizHawk.Emulation.Cores.csproj | 1 + .../Hardware/Datacorder/DatacorderDevice.cs | 32 ++- .../Machine/SpectrumBase.Media.cs | 5 + .../SinclairSpectrum/Media/MediaConverter.cs | 18 ++ .../Media/MediaConverterType.cs | 1 + .../Media/Tape/CSW/CswConverter.cs | 268 ++++++++++++++++++ .../Media/Tape/PZX/PzxConverter.cs | 2 +- .../Media/Tape/TZX/TzxConverter.cs | 43 ++- 12 files changed, 372 insertions(+), 7 deletions(-) create mode 100644 BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/CSW/CswConverter.cs diff --git a/BizHawk.Client.Common/RomGame.cs b/BizHawk.Client.Common/RomGame.cs index 315a3adbca..9badb6d7da 100644 --- a/BizHawk.Client.Common/RomGame.cs +++ b/BizHawk.Client.Common/RomGame.cs @@ -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 diff --git a/BizHawk.Client.EmuHawk/FileLoader.cs b/BizHawk.Client.EmuHawk/FileLoader.cs index a20c1a4222..da87ee0402 100644 --- a/BizHawk.Client.EmuHawk/FileLoader.cs +++ b/BizHawk.Client.EmuHawk/FileLoader.cs @@ -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" }; } diff --git a/BizHawk.Client.EmuHawk/MainForm.cs b/BizHawk.Client.EmuHawk/MainForm.cs index 2626719f67..b44096f7cc 100644 --- a/BizHawk.Client.EmuHawk/MainForm.cs +++ b/BizHawk.Client.EmuHawk/MainForm.cs @@ -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", "*.*"); } diff --git a/BizHawk.Emulation.Common/Database/Database.cs b/BizHawk.Emulation.Common/Database/Database.cs index 730acf9a05..6f92d4ddff 100644 --- a/BizHawk.Emulation.Common/Database/Database.cs +++ b/BizHawk.Emulation.Common/Database/Database.cs @@ -305,6 +305,7 @@ namespace BizHawk.Emulation.Common case ".TZX": case ".PZX": + case ".CSW": game.System = "ZXSpectrum"; break; diff --git a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj index 61b2dd874d..13dd091df7 100644 --- a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj +++ b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj @@ -293,6 +293,7 @@ + diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs index 1a4f787db2..8c69171110 100644 --- a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs @@ -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 diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Media.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Media.cs index 9a2a37b15c..d2f572af1b 100644 --- a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Media.cs +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Media.cs @@ -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; diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaConverter.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaConverter.cs index 631095ccd7..f9059c7e10 100644 --- a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaConverter.cs +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaConverter.cs @@ -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; } + /// + /// Decompresses a byte array that is Z-RLE compressed + /// + /// + /// + 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 } } diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaConverterType.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaConverterType.cs index 25b3245a20..ed89724f30 100644 --- a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaConverterType.cs +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaConverterType.cs @@ -10,6 +10,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum TZX, TAP, PZX, + CSW, DSK } } diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/CSW/CswConverter.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/CSW/CswConverter.cs new file mode 100644 index 0000000000..8f0ae34787 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/CSW/CswConverter.cs @@ -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 +{ + /// + /// Responsible for Compressed Square Wave conversion + /// https://web.archive.org/web/20171024182530/http://ramsoft.bbk.org.omegahg.com/csw.html + /// + public class CswConverter : MediaConverter + { + /// + /// The type of serializer + /// + private MediaConverterType _formatType = MediaConverterType.CSW; + public override MediaConverterType FormatType + { + get + { + return _formatType; + } + } + + /// + /// Position counter + /// + private int _position = 0; + + /// + /// Signs whether this class can be used to read the data format + /// + public override bool IsReader { get { return true; } } + + /// + /// Signs whether this class can be used to write the data format + /// + public override bool IsWriter { get { return false; } } + + #region Construction + + private DatacorderDevice _datacorder; + + public CswConverter(DatacorderDevice _tapeDevice) + { + _datacorder = _tapeDevice; + } + + #endregion + + /// + /// Returns TRUE if pzx header is detected + /// + /// + 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; + } + } + + /// + /// DeSerialization method + /// + /// + 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(); + + 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); + } + + /// + /// Processes a CSW v2 data block + /// + /// + /// + /// + /// + /// + 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"); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/PZX/PzxConverter.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/PZX/PzxConverter.cs index 016fd8da49..6ada823be8 100644 --- a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/PZX/PzxConverter.cs +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/PZX/PzxConverter.cs @@ -61,7 +61,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum #endregion /// - /// Returns TRUE if tzx header is detected + /// Returns TRUE if pzx header is detected /// /// public override bool CheckType(byte[] data) diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxConverter.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxConverter.cs index 320f2593ad..052be1824f 100644 --- a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxConverter.cs +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxConverter.cs @@ -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