diff --git a/BizHawk.Client.Common/RomGame.cs b/BizHawk.Client.Common/RomGame.cs index 13eaf5b94f..315a3adbca 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") + 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 diff --git a/BizHawk.Client.EmuHawk/FileLoader.cs b/BizHawk.Client.EmuHawk/FileLoader.cs index db3c00d76d..a20c1a4222 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" + ".EXE", ".PRG", ".D64", "*G64", ".CRT", ".TAP", ".32X", ".MDS", ".TZX", ".PZX" }; } diff --git a/BizHawk.Client.EmuHawk/MainForm.cs b/BizHawk.Client.EmuHawk/MainForm.cs index d839888927..2626719f67 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;%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", "*.*"); } diff --git a/BizHawk.Emulation.Common/Database/Database.cs b/BizHawk.Emulation.Common/Database/Database.cs index 052b1957cc..730acf9a05 100644 --- a/BizHawk.Emulation.Common/Database/Database.cs +++ b/BizHawk.Emulation.Common/Database/Database.cs @@ -304,6 +304,7 @@ namespace BizHawk.Emulation.Common break; case ".TZX": + case ".PZX": game.System = "ZXSpectrum"; break; diff --git a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj index 08933f73d1..61b2dd874d 100644 --- a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj +++ b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj @@ -293,10 +293,11 @@ + - - + + diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs index 6701d0a923..ff80df1982 100644 --- a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs @@ -320,8 +320,12 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum /// 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]; diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Media.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Media.cs index 5d38566152..9a2a37b15c 100644 --- a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Media.cs +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Media.cs @@ -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; diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaConverterType.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaConverterType.cs index 034e45e5d0..25b3245a20 100644 --- a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaConverterType.cs +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaConverterType.cs @@ -9,6 +9,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum NONE, TZX, TAP, + PZX, DSK } } diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/PZX/PzxConverter.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/PZX/PzxConverter.cs new file mode 100644 index 0000000000..1d913aef8c --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/PZX/PzxConverter.cs @@ -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 +{ + /// + /// Reponsible for PZX format serializaton + /// Based on the information here: http://zxds.raxoft.cz/docs/pzx.txt + /// + public class PzxConverter : MediaConverter + { + /// + /// The type of serializer + /// + private MediaConverterType _formatType = MediaConverterType.PZX; + public override MediaConverterType FormatType + { + get + { + return _formatType; + } + } + + /// + /// 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; } } + + /// + /// Working list of generated tape data blocks + /// + private List _blocks = new List(); + + /// + /// Position counter + /// + private int _position = 0; + + /// + /// Object to keep track of loops - this assumes there is only one loop at a time + /// + private List> _loopCounter = new List>(); + + #region Construction + + private DatacorderDevice _datacorder; + + public PzxConverter(DatacorderDevice _tapeDevice) + { + _datacorder = _tapeDevice; + } + + #endregion + + /// + /// Returns TRUE if tzx header is detected + /// + /// + 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; + } + } + + /// + /// DeSerialization method + /// + /// + 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 bDatas = new List(); + + 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(); + + List pulses = new List(); + + 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(); + + List s0 = new List(); + List s1 = new List(); + List dData = new List(); + + 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 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(); + + 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(); + + 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; + } + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapConverter.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapConverter.cs similarity index 100% rename from BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapConverter.cs rename to BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapConverter.cs diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TzxConverter.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxConverter.cs similarity index 100% rename from BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TzxConverter.cs rename to BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxConverter.cs diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeDataBlock.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeDataBlock.cs index 0b1c4e910f..ed3c046fdc 100644 --- a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeDataBlock.cs +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeDataBlock.cs @@ -113,6 +113,8 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum /// public List DataPeriods = new List(); + public List InitialPulseLevel = new List(); + /// /// 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 + } + /// /// Different title possibilities